diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d18061a7..44770b85 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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/ diff --git a/src/app/_helpers/service-handler.interceptor.ts b/src/app/_helpers/service-handler.interceptor.ts new file mode 100644 index 00000000..9befe749 --- /dev/null +++ b/src/app/_helpers/service-handler.interceptor.ts @@ -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, + next: HttpHandler, + ): Observable> { + 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); + }), + ); + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 05d442f7..485c2ccb 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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 { diff --git a/src/app/modules/auth/ng-http.service.ts b/src/app/modules/auth/ng-http.service.ts index 54b1d523..73032f69 100644 --- a/src/app/modules/auth/ng-http.service.ts +++ b/src/app/modules/auth/ng-http.service.ts @@ -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; + switch (settings.method) { case 'GET': - return this.http - .get(settings.url, {headers: this.getHeaders(settings.headers)}) - .toPromise(); + observable = this.http.get(settings.url, { + headers: this.getHeaders(settings.headers), + }); + break; case 'POST': - return this.http - .post(settings.url, settings.data, { - headers: this.getHeaders(settings.headers), - }) - .toPromise(); + observable = this.http.post(settings.url, settings.data, { + headers: this.getHeaders(settings.headers), + }); + break; case 'PUT': - return this.http - .put(settings.url, settings.data, { - headers: this.getHeaders(settings.headers), - }) - .toPromise(); + observable = this.http.put(settings.url, settings.data, { + headers: this.getHeaders(settings.headers), + }); + break; case 'DELETE': - return this.http - .delete(settings.url, {headers: this.getHeaders(settings.headers)}) - .toPromise(); + observable = this.http.delete(settings.url, { + headers: this.getHeaders(settings.headers), + }); + break; } + + return observable.toPromise(); } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/app/modules/data/elements/skeleton-simple-card.component.ts b/src/app/modules/data/elements/skeleton-simple-card.component.ts index 3c00da6a..d7d0ffa7 100644 --- a/src/app/modules/data/elements/skeleton-simple-card.component.ts +++ b/src/app/modules/data/elements/skeleton-simple-card.component.ts @@ -12,7 +12,7 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -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; +} diff --git a/src/app/modules/data/elements/skeleton-simple-card.html b/src/app/modules/data/elements/skeleton-simple-card.html index 2e51180b..4ad421a3 100644 --- a/src/app/modules/data/elements/skeleton-simple-card.html +++ b/src/app/modules/data/elements/skeleton-simple-card.html @@ -1,8 +1,14 @@ - + -

+

+ +

diff --git a/src/app/modules/library/account/account.page.html b/src/app/modules/library/account/account.page.html index 1971689f..0e89587c 100644 --- a/src/app/modules/library/account/account.page.html +++ b/src/app/modules/library/account/account.page.html @@ -9,11 +9,14 @@ -

- {{ 'library.account.greeting' | translate | titlecase }} +

+ {{ 'library.account.greeting' | translate }} {{ name | firstLastName }}! {{ 'library.account.login.success' | translate }}

+ +

+
{{ 'library.account.pages.profile.title' | translate | titlecase }} diff --git a/src/app/modules/library/account/account.page.ts b/src/app/modules/library/account/account.page.ts index 0cad4ac4..93d9d0db 100644 --- a/src/app/modules/library/account/account.page.ts +++ b/src/app/modules/library/account/account.page.ts @@ -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 { - try { - const patron = await this.libraryAccountService.getProfile(); - this.name = patron.name; - } catch { - // this.router.navigate(['profile']); - } + const patron = await this.libraryAccountService.getProfile(); + this.name = patron?.name; } } diff --git a/src/app/modules/library/account/checked-out/checked-out-page.component.html b/src/app/modules/library/account/checked-out/checked-out-page.component.html deleted file mode 100644 index 9fc0dce8..00000000 --- a/src/app/modules/library/account/checked-out/checked-out-page.component.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - {{ - 'library.account.pages.checked_out.title' | translate | titlecase - }} - - - - - - - - - diff --git a/src/app/modules/library/account/checked-out/checked-out-page.component.ts b/src/app/modules/library/account/checked-out/checked-out-page.component.ts index 98cdb169..bcaa7526 100644 --- a/src/app/modules/library/account/checked-out/checked-out-page.component.ts +++ b/src/app/modules/library/account/checked-out/checked-out-page.component.ts @@ -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 { + 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 = []; } } } diff --git a/src/app/modules/library/account/checked-out/checked-out-page.html b/src/app/modules/library/account/checked-out/checked-out-page.html new file mode 100644 index 00000000..8aecccb7 --- /dev/null +++ b/src/app/modules/library/account/checked-out/checked-out-page.html @@ -0,0 +1,38 @@ + + + + + + + {{ + 'library.account.pages.checked_out.title' | translate | titlecase + }} + + + + + + + + + + + + + {{ 'search.nothing_found' | translate | titlecase }} + + + + diff --git a/src/app/modules/library/account/checked-out/checked-out-page.component.scss b/src/app/modules/library/account/checked-out/checked-out-page.scss similarity index 100% rename from src/app/modules/library/account/checked-out/checked-out-page.component.scss rename to src/app/modules/library/account/checked-out/checked-out-page.scss diff --git a/src/app/modules/library/account/elements/fee-item/fee-item.component.ts b/src/app/modules/library/account/elements/fee-item/fee-item.component.ts new file mode 100644 index 00000000..83542b33 --- /dev/null +++ b/src/app/modules/library/account/elements/fee-item/fee-item.component.ts @@ -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) {} +} diff --git a/src/app/modules/library/account/elements/fee-item/fee-item.html b/src/app/modules/library/account/elements/fee-item/fee-item.html new file mode 100644 index 00000000..dbceecba --- /dev/null +++ b/src/app/modules/library/account/elements/fee-item/fee-item.html @@ -0,0 +1,23 @@ + + +

+ {{ 'library.account.pages.fines.labels.edition' | translate }}: + {{ book.name }} +

+

+

+ +

+ {{ 'library.account.pages.fines.labels' + '.' + property | translate }}: + + {{ fee[property] }} + + + {{ fee[property] | amDateFormat: 'll' }} + +

+
+
diff --git a/src/app/modules/library/account/fines/fines-page.component.scss b/src/app/modules/library/account/elements/fee-item/fee-item.scss similarity index 100% rename from src/app/modules/library/account/fines/fines-page.component.scss rename to src/app/modules/library/account/elements/fee-item/fee-item.scss diff --git a/src/app/modules/library/account/elements/paia-item/paiaitem.component.html b/src/app/modules/library/account/elements/paia-item/paiaitem.component.html deleted file mode 100644 index 7943f14e..00000000 --- a/src/app/modules/library/account/elements/paia-item/paiaitem.component.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - {{ book.name }} - - - - - {{ - 'library.account.pages' + - '.' + - pageName + - '.' + - 'labels' + - '.' + - property - | translate - | titlecase - }}: - {{ item[property] }} - - - - diff --git a/src/app/modules/library/account/elements/paia-item/paiaitem.component.ts b/src/app/modules/library/account/elements/paia-item/paiaitem.component.ts index e366b7d9..e08546e6 100644 --- a/src/app/modules/library/account/elements/paia-item/paiaitem.component.ts +++ b/src/app/modules/library/account/elements/paia-item/paiaitem.component.ts @@ -13,43 +13,27 @@ * this program. If not, see . */ -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 = new EventEmitter(); + + async onClick(action: DocumentAction['action']) { + this.documentAction.emit({doc: this.item, action}); + } } diff --git a/src/app/modules/library/account/elements/paia-item/paiaitem.html b/src/app/modules/library/account/elements/paia-item/paiaitem.html new file mode 100644 index 00000000..3f6a8703 --- /dev/null +++ b/src/app/modules/library/account/elements/paia-item/paiaitem.html @@ -0,0 +1,58 @@ + + + + +

{{ item.about }}

+ +

+ {{ + 'library.account.pages' + + '.' + + listName + + '.' + + 'labels' + + '.' + + property | translate + }}: + + {{ item[property] }} + + + {{ item[property] | amDateFormat: 'll' }} + +

+
+ + + {{ 'library.account.actions.cancel.header' | translate }} + + {{ 'library.account.actions.renew.header' | translate }} + +
+
diff --git a/src/app/modules/library/account/elements/paia-item/paiaitem.component.scss b/src/app/modules/library/account/elements/paia-item/paiaitem.scss similarity index 100% rename from src/app/modules/library/account/elements/paia-item/paiaitem.component.scss rename to src/app/modules/library/account/elements/paia-item/paiaitem.scss diff --git a/src/app/modules/library/account/fines/fines-page.component.html b/src/app/modules/library/account/fines/fines-page.component.html deleted file mode 100644 index 66d6c0cb..00000000 --- a/src/app/modules/library/account/fines/fines-page.component.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - {{ - 'library.account.pages.fines.title' | translate | titlecase - }} - - - - - {{ fine.about }} - - - - - {{ - 'library.account.pages.fines.labels' + '.' + property - | translate - | titlecase - }}: - {{ fine[property] }} - - - - - - - - {{ - 'library.account.pages.fines.labels.amount' | translate | titlecase - }}: - - - {{ amount }} - - - - diff --git a/src/app/modules/library/account/fines/fines-page.component.ts b/src/app/modules/library/account/fines/fines-page.component.ts index 59199f4a..f530bc12 100644 --- a/src/app/modules/library/account/fines/fines-page.component.ts +++ b/src/app/modules/library/account/fines/fines-page.component.ts @@ -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 { - 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 { 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(); + } + } } diff --git a/src/app/modules/library/account/fines/fines-page.html b/src/app/modules/library/account/fines/fines-page.html new file mode 100644 index 00000000..bcbe5a13 --- /dev/null +++ b/src/app/modules/library/account/fines/fines-page.html @@ -0,0 +1,40 @@ + + + + + + + {{ + 'library.account.pages.fines.title' | translate | titlecase + }} + + + + + + + + + + + + + + {{ 'library.account.pages.fines.labels.total_amount' | translate }}: + + + {{ amount }} + + + + + + + + + + + diff --git a/src/app/modules/library/account/holds-and-reservations/holds-and-reservations-page.component.scss b/src/app/modules/library/account/fines/fines-page.scss similarity index 100% rename from src/app/modules/library/account/holds-and-reservations/holds-and-reservations-page.component.scss rename to src/app/modules/library/account/fines/fines-page.scss diff --git a/src/app/modules/library/account/holds-and-reservations/holds-and-reservations-page.component.html b/src/app/modules/library/account/holds-and-reservations/holds-and-reservations-page.component.html deleted file mode 100644 index 8cf8e663..00000000 --- a/src/app/modules/library/account/holds-and-reservations/holds-and-reservations-page.component.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - {{ - 'library.account.pages.holds.title' | translate | titlecase - }} - - - - - - - - diff --git a/src/app/modules/library/account/holds-and-reservations/holds-and-reservations-page.component.ts b/src/app/modules/library/account/holds-and-reservations/holds-and-reservations-page.component.ts index f41d45bb..9a4bdb85 100644 --- a/src/app/modules/library/account/holds-and-reservations/holds-and-reservations-page.component.ts +++ b/src/app/modules/library/account/holds-and-reservations/holds-and-reservations-page.component.ts @@ -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 { + 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(); + } } diff --git a/src/app/modules/library/account/holds-and-reservations/holds-and-reservations-page.html b/src/app/modules/library/account/holds-and-reservations/holds-and-reservations-page.html new file mode 100644 index 00000000..2176be45 --- /dev/null +++ b/src/app/modules/library/account/holds-and-reservations/holds-and-reservations-page.html @@ -0,0 +1,53 @@ + + + + + + + {{ + 'library.account.pages.holds.title' | translate | titlecase + }} + + + + + + {{ + 'library.account.pages.holds.holds' | translate + }} + + + {{ + 'library.account.pages.holds.reservations' | translate + }} + + + + + + + + + + + {{ 'search.nothing_found' | translate | titlecase }} + + + + diff --git a/src/app/modules/library/account/profile/profile-page.component.scss b/src/app/modules/library/account/holds-and-reservations/holds-and-reservations-page.scss similarity index 100% rename from src/app/modules/library/account/profile/profile-page.component.scss rename to src/app/modules/library/account/holds-and-reservations/holds-and-reservations-page.scss diff --git a/src/app/modules/library/account/library-account.service.ts b/src/app/modules/library/account/library-account.service.ts index cba00b55..cc6b4be7 100644 --- a/src/app/modules/library/account/library-account.service.ts +++ b/src/app/modules/library/account/library-account.service.ts @@ -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(`${this.baseUrl}/{patron}/fees`); } - private async performRequest(urlTemplate: string): Promise { + private async performRequest( + urlTemplate: string, + method = 'GET', + data?: JQuery.PlainObject, + ): Promise { 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( - { - query: this.getRawId(edition) as string, - page: 0, - }, - {addPrefix: true}, - ); - return response.data[0]; + 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( + `${this.baseUrl}/{patron}/cancel`, + 'POST', + {doc: [document]}, + ); + } + + renewLanding(document: PAIADocument) { + return this.performRequest(`${this.baseUrl}/{patron}/renew`, 'POST', { + doc: [document], + }); + } + + async handleDocumentAction(documentAction: DocumentAction): Promise { + 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(); } } diff --git a/src/app/modules/library/account/profile/profile-page.component.html b/src/app/modules/library/account/profile/profile-page.component.html deleted file mode 100644 index de568955..00000000 --- a/src/app/modules/library/account/profile/profile-page.component.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - {{ - 'library.account.pages.profile.title' | translate | titlecase - }} - - - - - - - - {{ - 'library.account.pages.profile.labels' + '.' + property - | translate - | titlecase - }}: - - - {{ patron[property] }} - - - - - diff --git a/src/app/modules/library/account/profile/profile-page.component.ts b/src/app/modules/library/account/profile/profile-page.component.ts index f6117078..e048a5c3 100644 --- a/src/app/modules/library/account/profile/profile-page.component.ts +++ b/src/app/modules/library/account/profile/profile-page.component.ts @@ -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 { try { this.patron = await this.libraryAccountService.getProfile(); - console.log(this.patron); } catch { - // TODO: error handling + await this.libraryAccountService.handleError(); } } } diff --git a/src/app/modules/library/account/profile/profile-page.html b/src/app/modules/library/account/profile/profile-page.html new file mode 100644 index 00000000..32a5a182 --- /dev/null +++ b/src/app/modules/library/account/profile/profile-page.html @@ -0,0 +1,41 @@ + + + + + + + {{ + 'library.account.pages.profile.title' | translate | titlecase + }} + + + + + + + + + + {{ + 'library.account.pages.profile.labels' + '.' + property + | translate + }}: + + + + {{ patron[property] }} + + + {{ patron[property] | amDateFormat: 'll' }} + + + + + + + + + + + + diff --git a/src/app/modules/library/account/profile/profile-page.scss b/src/app/modules/library/account/profile/profile-page.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/modules/library/library.module.ts b/src/app/modules/library/library.module.ts index f0cd282a..3b89d5e6 100644 --- a/src/app/modules/library/library.module.ts +++ b/src/app/modules/library/library.module.ts @@ -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 {} diff --git a/src/app/modules/library/types.ts b/src/app/modules/library/types.ts index c230c0fe..806395ee 100644 --- a/src/app/modules/library/types.ts +++ b/src/app/modules/library/types.ts @@ -67,3 +67,8 @@ export enum PAIADocumentStatus { Provided = 4, Rejected = 5, } + +export interface DocumentAction { + action: 'cancel' | 'renew'; + doc: PAIADocument; +} diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index cd3c78f7..9fab6acf 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -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" + } } } }, diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index aedac993..9d31fcbf 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -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" + } } } },