mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-07 14:02:48 +00:00
refactor: improve library account views
This commit is contained in:
@@ -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/
|
||||
|
||||
36
src/app/_helpers/service-handler.interceptor.ts
Normal file
36
src/app/_helpers/service-handler.interceptor.ts
Normal 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);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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 = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
40
src/app/modules/library/account/fines/fines-page.html
Normal file
40
src/app/modules/library/account/fines/fines-page.html
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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> {
|
||||
try {
|
||||
this.holds = await this.libraryAccountService.getHoldsAndReservations();
|
||||
} catch {
|
||||
// TODO: error handling
|
||||
await this.fetchItems();
|
||||
}
|
||||
|
||||
async fetchItems(status: PAIADocumentStatus = PAIADocumentStatus.Ordered) {
|
||||
this.paiaDocuments = undefined;
|
||||
try {
|
||||
this.paiaDocuments = await (Number(status) === PAIADocumentStatus.Ordered
|
||||
? this.libraryAccountService.getHolds()
|
||||
: this.libraryAccountService.getReservations());
|
||||
} catch {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
41
src/app/modules/library/account/profile/profile-page.html
Normal file
41
src/app/modules/library/account/profile/profile-page.html
Normal 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>
|
||||
@@ -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 {}
|
||||
|
||||
@@ -67,3 +67,8 @@ export enum PAIADocumentStatus {
|
||||
Provided = 4,
|
||||
Rejected = 5,
|
||||
}
|
||||
|
||||
export interface DocumentAction {
|
||||
action: 'cancel' | 'renew';
|
||||
doc: PAIADocument;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user