LP1816475: Improving booking return, adding validation
authorJane Sandberg <sandbej@linnbenton.edu>
Sat, 30 Mar 2019 20:19:29 +0000 (13:19 -0700)
committerJane Sandberg <sandbej@linnbenton.edu>
Wed, 17 Apr 2019 20:23:02 +0000 (13:23 -0700)
Signed-off-by: Jane Sandberg <sandbej@linnbenton.edu>
17 files changed:
Open-ILS/src/eg2/src/app/core/format.service.ts
Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.html
Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.ts
Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html
Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts
Open-ILS/src/eg2/src/app/staff/booking/pickup.component.html
Open-ILS/src/eg2/src/app/staff/booking/pickup.component.ts
Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.html
Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts
Open-ILS/src/eg2/src/app/staff/booking/return.component.html
Open-ILS/src/eg2/src/app/staff/booking/return.component.ts
Open-ILS/src/eg2/src/app/staff/booking/routing.module.ts
Open-ILS/src/eg2/src/app/staff/nav.component.html
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.data.booking-sticky-settings.sql
Open-ILS/src/templates/staff/circ/patron/index.tt2
Open-ILS/src/templates/staff/navbar.tt2

index fddbed2..28085e3 100644 (file)
@@ -146,13 +146,15 @@ export class FormatService {
 
     momentizeDateString(date: string, timezone: string): Moment {
         const parseableFormat = this.makeFormatParseable(this.dateFormat);
-        if (parseableFormat.length) {return Moment(date, parseableFormat, timezone)};
-        return Moment(date, timezone);
+        if (parseableFormat.length) { return Moment.tz(date, parseableFormat, timezone); }
+        // TODO: The following fallback returns the date at midnight UTC,
+        // rather than midnight in the local TZ
+        return Moment.tz(date, timezone);
     }
 
     momentizeDateTimeString(datetime: string, timezone: string): Moment {
         const parseableFormat = this.makeFormatParseable(this.dateTimeFormat);
-        if (parseableFormat.length) {return Moment(datetime, parseableFormat, timezone)};
+        if (parseableFormat.length) { return Moment(datetime, parseableFormat, timezone); }
         return Moment(datetime, timezone);
     }
 
index 4848ded..891079a 100644 (file)
@@ -1,3 +1,4 @@
+<span class="material-icons" *ngIf="validatorError">error</span>
 <div class="input-group">
   <input type="datetime"
     [attr.id]="domId.length ? domId : null" 
     Cannot edit this date or time.
   </div>
   <div *ngIf="!readOnly">
+    <div *ngIf="validatorError" class="alert alert-danger">
+      <span class="material-icons">error</span>
+      {{validatorError}}
+    </div>
     <ngb-datepicker
       [(ngModel)]="dateModel"
       (ngModelChange)="modelChanged()"
index de8fa86..a12a84f 100644 (file)
@@ -14,14 +14,16 @@ export class DateTimeSelectComponent implements OnInit {
     @Input() domId = '';
     @Input() fieldName: string;
     @Input() required: boolean;
-    @Input() minuteStep: number = 15;
+    @Input() minuteStep = 15;
     @Input() showTZ = true;
     @Input() timezone: string = this.format.wsOrgTimezone;
     @Input() readOnly = false;
+    @Input() validatorError = '';
 
     @Input() initialIso: string;
 
-    @Output() onChangeAsIso = new EventEmitter();
+    @Output() onChangeAsIso = new EventEmitter<string>();
+    @Output() onChangeAsMoment = new EventEmitter<Moment>();
 
     stringVersion: any; // Used internally on internal input
     timeModel: NgbTimeStruct;
@@ -70,10 +72,10 @@ export class DateTimeSelectComponent implements OnInit {
         }
 
         if (newDate && !isNaN(newDate)) {
-            console.log('newDate');
             // Set component view value
-           this.stringVersion = this.format.transform({value: newDate, datatype: 'timestamp', datePlusTime: true});
+            this.stringVersion = this.format.transform({value: newDate, datatype: 'timestamp', datePlusTime: true});
             // Update form passed in view value
+            this.onChangeAsMoment.emit(newDate);
             this.onChangeAsIso.emit(newDate.toISOString());
         }
     }
index 747ac14..583bef0 100644 (file)
@@ -50,7 +50,8 @@
                 [showTZ]="timezone"
                 [timezone]="timezone"
                 domId="{{idPrefix}}-{{field.name}}"
-                (onChangeAsIso)="record[field.name]($event)"
+                (onChangeAsMoment)="record[field.name]($event)"
+                [validatorError]="field.validatorError"
                 [readOnly]="field.readOnly"
                 initialIso="{{record[field.name]()}}">
               </eg-datetime-select>
index 789895c..1b13d8c 100644 (file)
@@ -59,6 +59,13 @@ export interface FmFieldOptions {
     // This supersedes all other isRequired specifiers.
     isRequiredOverride?: (field: string, record: IdlObject) => boolean;
 
+    // If this function is defined, the function will be called
+    // when fields change their values, to check if users are entering
+    // valid values, and delivering an error message if not.
+    // 
+    // Currently only implemented for the datetime-select widget
+    validator?: (field: string, value: any, record: IdlObject) => string;
+
     // Directly apply the readonly status of the field.
     // This only has an affect if the value is true.
     isReadonly?: boolean;
@@ -324,6 +331,16 @@ export class FmRecordEditorComponent
             || fieldOptions.isReadonly === true
             || this.readonlyFieldsList.includes(field.name);
 
+        if (fieldOptions.validator) {
+            field.validator = fieldOptions.validator;
+        } else {
+            field.validator = (fieldName: string, value: any, record: IdlObject) => { return ''; }
+        }
+
+        field.validate = (fieldName: string, value: any, record: IdlObject) => {
+            field.validatorError = field.validator(fieldName, value, record); }
+
+
         if (fieldOptions.isRequiredOverride) {
             field.isRequired = () => {
                 return fieldOptions.isRequiredOverride(field.name, this.record);
@@ -526,5 +543,6 @@ export class FmRecordEditorComponent
         // datatype == text / interval / editable-pkey
         return 'text';
     }
+
 }
 
index 34b236f..5073d79 100644 (file)
     <input class="form-check-input" type="checkbox" [checked]="onlyShowCaptured" id="only-show-captured" (change)="handleShowCapturedChange()">
     <label class="form-check-label" for="only-show-captured" i18n>Show only captured resources</label>
   </div>
-  <eg-grid #readyGrid [dataSource]="readySource"
-    (onRowActivate)="pickup($event).subscribe()"
-    [sortable]="true" persistKey="booking.pull_list" >
-    <eg-grid-toolbar-action label="Pick up" i18n-label [action]="pickupSelected" [disableOnRows]="noSelectedRows"></eg-grid-toolbar-action>
-    <eg-grid-column name="id" [hidden]="true" [index]="true" i18n-label label="ID" path="id"></eg-grid-column>
-    <eg-grid-column name="barcode" label="Barcode" i18n-label path="current_resource.barcode"></eg-grid-column>
-    <eg-grid-column name="title" label="Title or name" i18n-label path="target_resource_type.name"></eg-grid-column>
-    <eg-grid-column label="Reservation start time" [datePlusTime]="true" path="reservations.0.start_time" i18n-label></eg-grid-column>
-    <eg-grid-column label="Reservation end time" [datePlusTime]="true" path="reservations.0.end_time" i18n-label></eg-grid-column>
-
-  </eg-grid>
+  <eg-reservations-grid #readyGrid [patron]="patronId" status="pickupReady" [onlyCaptured]="onlyShowCaptured" persistSuffix="pickup.ready" (onPickup)="this.pickedUpGrid.reloadGrid()"></eg-reservations-grid>
 
   <h2 class="text-center mt-2" i18n>Already picked up</h2>
   <eg-reservations-grid #pickedUpGrid [patron]="patronId" status="pickedUp" persistSuffix="pickup.picked_up"></eg-reservations-grid>
index 57b54b1..610b2b7 100644 (file)
@@ -1,5 +1,4 @@
 import { Component, Input, OnInit, ViewChild } from '@angular/core';
-import { GridDataSource } from '@eg/share/grid/grid';
 import { Pager } from '@eg/share/util/pager';
 import {PatronService} from '@eg/staff/share/patron.service';
 import {PcrudService} from '@eg/core/pcrud.service';
@@ -8,7 +7,6 @@ import {IdlObject} from '@eg/core/idl.service';
 import {NetService} from '@eg/core/net.service';
 import {Observable} from 'rxjs';
 import {single, tap} from 'rxjs/operators';
-import {GridComponent} from '@eg/share/grid/grid.component';
 import {ReservationsGridComponent} from './reservations-grid.component';
 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
 import {ServerStoreService} from '@eg/core/server-store.service';
@@ -24,13 +22,10 @@ export class PickupComponent implements OnInit {
     patronId: number;
     retrievePatron: () => void;
 
-    @ViewChild('readyGrid') readyGrid: GridComponent;
+    @ViewChild('readyGrid') readyGrid: ReservationsGridComponent;
     @ViewChild('pickedUpGrid') pickedUpGrid: ReservationsGridComponent;
 
-    public readySource: GridDataSource;
     noSelectedRows: (rows: IdlObject[]) => boolean;
-    pickupSelected: (reservations: IdlObject[]) => void;
-    pickup: (reservation: IdlObject) => Observable<any>;
 
     onlyShowCaptured = true;
     handleShowCapturedChange: () => void;
@@ -42,7 +37,7 @@ export class PickupComponent implements OnInit {
         private patron: PatronService,
         private route: ActivatedRoute,
         private router: Router,
-       private store: ServerStoreService,
+        private store: ServerStoreService,
         private toast: ToastService
     ) {
     }
@@ -60,36 +55,12 @@ export class PickupComponent implements OnInit {
             }).subscribe(
                 (resp) => {
                     this.patronBarcode = resp.card().barcode();
-                    this.readyGrid.reload();
+                    this.readyGrid.reloadGrid();
                     this.pickedUpGrid.reloadGrid();
                 }, (err) => { console.debug(err); }
             );
         });
 
-        this.readySource = new GridDataSource();
-        this.readySource.getRows = (pager: Pager, sort: any[]) => {
-            const orderBy: any = {};
-            let where = {
-                'usr' : this.patronId,
-                'pickup_time' : null,
-                'start_time' : {'!=': null},
-                'cancel_time' : null
-            };
-            if (this.onlyShowCaptured) {
-                where['capture_time'] = {'!=': null};
-            }
-
-            return this.pcrud.search('bresv', where,  {
-                order_by: orderBy,
-                limit: pager.limit,
-                offset: pager.offset,
-                flesh: 1,
-                flesh_fields: {'bresv' : [
-                    'usr', 'capture_staff', 'target_resource', 'target_resource_type', 'current_resource', 'request_lib', 'pickup_lib'
-                ] }
-            });
-
-        };
         this.retrievePatron = () => {
             if (this.patronBarcode) {
                 this.patron.bcSearch(this.patronBarcode).pipe(single()).subscribe(
@@ -98,36 +69,13 @@ export class PickupComponent implements OnInit {
                 );
             }
         };
-        this.noSelectedRows = (rows: IdlObject[]) => (rows.length === 0);
-
-        this.pickupSelected = (reservations: IdlObject[]) => {
-            const pickupOne = (thing: IdlObject) => {
-                if (!thing) { return; }
-                this.pickup(thing).subscribe(
-                    () => pickupOne(reservations.shift()));
-            };
-            pickupOne(reservations.shift());
-        };
 
-        this.pickup = (reservation: IdlObject) => {
-            return this.net.request(
-               'open-ils.circ',
-               'open-ils.circ.reservation.pickup',
-               this.auth.token(),
-               {'patron_barcode': this.patronBarcode, 'reservation': reservation})
-               .pipe(tap(
-                   (success) => {
-                       this.readyGrid.reload();
-                       this.pickedUpGrid.reloadGrid(); },
-                   (error) => { console.debug(error); }
-               ));
-        };
         this.store.getItem('eg.booking.pickup.ready.only_show_captured').then(onlyCaptured => {
             if (onlyCaptured != null) { this.onlyShowCaptured = onlyCaptured; }
         });
         this.handleShowCapturedChange = () => {
             this.onlyShowCaptured = !this.onlyShowCaptured;
-            this.readyGrid.reload();
+            this.readyGrid.reloadGrid();
             this.store.setItem('eg.booking.pickup.ready.only_show_captured', this.onlyShowCaptured);
         };
 
index f2ed3ba..9fd3187 100644 (file)
@@ -1,14 +1,15 @@
 <eg-grid #grid [dataSource]="gridSource"
-  (onRowActivate)="showEditDialog($event)"
+  (onRowActivate)="handleRowActivate($event)"
   [sortable]="true" persistKey="booking.{{persistSuffix}}" >
   <eg-grid-toolbar-action label="Edit Selected" i18n-label [action]="editSelected" [disableOnRows]="noSelectedRows"></eg-grid-toolbar-action>
   <eg-grid-toolbar-action label="Cancel Selected" i18n-label [action]="cancelSelected" [disableOnRows]="cancelNotAppropriate"></eg-grid-toolbar-action>
+  <eg-grid-toolbar-action label="Pick Up Selected" i18n-label [action]="pickupSelected" [disableOnRows]="pickupNotAppropriate"></eg-grid-toolbar-action>
   <eg-grid-toolbar-action label="Return Selected" i18n-label [action]="returnSelected" [disableOnRows]="returnNotAppropriate"></eg-grid-toolbar-action>
   <eg-grid-toolbar-action label="View Reservations for This Patron" i18n-label [action]="viewByPatron" [disableOnRows]="notOnePatronSelected"></eg-grid-toolbar-action>
   <eg-grid-toolbar-action label="View Reservations for This Resource" i18n-label [action]="viewByResource" [disableOnRows]="notOneResourceSelected"></eg-grid-toolbar-action>
 
   <eg-grid-column name="id" [hidden]="true" [index]="true" i18n-label label="ID" path="id"></eg-grid-column>
-  <eg-grid-column label="Patron username" i18n-label path="usr.usrname" [sortable]="false"></eg-grid-column>
+  <eg-grid-column label="Patron username" [hidden]="true" i18n-label path="usr.usrname" [sortable]="false"></eg-grid-column>
   <eg-grid-column label="Patron barcode" i18n-label path="usr.card.barcode" [sortable]="false"></eg-grid-column>
   <eg-grid-column label="Patron first name" i18n-label  path="usr.first_given_name"></eg-grid-column>
   <eg-grid-column label="Patron middle name" i18n-label [hidden]="true" path="usr.second_given_name"></eg-grid-column>
@@ -39,6 +40,7 @@
   idlClass="bresv"
   datetimeFields="start_time,end_time"
   hiddenFields="xact_finish,cancel_time,booking_interval"
+  [fieldOptions]="{start_time:{validator:startTimeShouldBeFuture}}"
   [readonlyFields]="listReadOnlyFields()">
 </eg-fm-record-editor>
 <eg-confirm-dialog #confirmCancelReservationDialog
index 6b25b96..f040f6d 100644 (file)
@@ -1,4 +1,4 @@
-import {Component, Input, OnInit, ViewChild} from '@angular/core';
+import {Component, EventEmitter, Input, Output, OnInit, ViewChild} from '@angular/core';
 import {Observable} from 'rxjs';
 import {tap} from 'rxjs/operators';
 import {AuthService} from '@eg/core/auth.service';
@@ -29,8 +29,11 @@ export class ReservationsGridComponent implements OnInit {
     @Input() patron: number;
     @Input() resource: number;
     @Input() resourceType: number;
-    @Input() status: 'pickedUp' | 'returnedToday';
+    @Input() status: 'pickupReady' | 'pickedUp' | 'returnReady' | 'returnedToday';
     @Input() persistSuffix: string;
+    @Input() onlyCaptured = false;
+
+    @Output() onPickup = new EventEmitter<IdlObject>();
 
     gridSource: GridDataSource;
     patronBarcode: string;
@@ -43,6 +46,8 @@ export class ReservationsGridComponent implements OnInit {
     @ViewChild('noTimezoneSetDialog') noTimezoneSetDialog: NoTimezoneSetComponent;
 
     editSelected: (rows: IdlObject[]) => void;
+    pickupSelected: (rows: IdlObject[]) => void;
+    pickupResource: (rows: IdlObject) => Observable<any>;
     returnSelected: (rows: IdlObject[]) => void;
     returnResource: (rows: IdlObject) => Observable<any>;
     cancelSelected: (rows: IdlObject[]) => void;
@@ -52,12 +57,17 @@ export class ReservationsGridComponent implements OnInit {
     filterByCurrentResourceBarcode: () => void;
     listReadOnlyFields: () => string;
 
+    startTimeShouldBeFuture: (fieldName: string, value: Moment, record: IdlObject) => string;
+
+    handleRowActivate: (row: IdlObject) => void;
+
     reloadGrid: () => void;
 
     noSelectedRows: (rows: IdlObject[]) => boolean;
     notOnePatronSelected: (rows: IdlObject[]) => boolean;
     notOneResourceSelected: (rows: IdlObject[]) => boolean;
     cancelNotAppropriate: (rows: IdlObject[]) => boolean;
+    pickupNotAppropriate: (rows: IdlObject[]) => boolean;
     returnNotAppropriate: (rows: IdlObject[]) => boolean;
 
     constructor(
@@ -79,6 +89,7 @@ export class ReservationsGridComponent implements OnInit {
             this.noTimezoneSetDialog.open();
         }
 
+
         this.gridSource = new GridDataSource();
 
         this.gridSource.getRows = (pager: Pager, sort: any[]) => {
@@ -92,8 +103,15 @@ export class ReservationsGridComponent implements OnInit {
             if (this.resource) {
                 where['current_resource'] = this.resource;
             }
+            if (this.onlyCaptured) {
+                where['capture_time'] = {'!=': null};
+            }
+
             if (this.status) {
-                if ('pickedUp' === this.status) {
+                if ('pickupReady' === this.status) {
+                    where['pickup_time'] = null;
+                    where['start_time'] = {'!=': null};
+                } else if ('pickedUp' === this.status || 'returnReady' === this.status) {
                     where['pickup_time'] = {'!=': null};
                     where['return_time'] = null;
                 } else if ('returnedToday' === this.status) {
@@ -112,7 +130,7 @@ export class ReservationsGridComponent implements OnInit {
                     'usr', 'capture_staff', 'target_resource', 'target_resource_type', 'current_resource', 'request_lib', 'pickup_lib'
                 ], 'au': ['card'] }
             }).pipe(tap((row) => {
-                this.org.settings('lib.timezone', row['pickup_lib']()).then((tz) => {row['timezone'] = tz['lib.timezone']});
+                this.org.settings('lib.timezone', row['pickup_lib']()).then((tz) => {row['timezone'] = tz['lib.timezone'];});
             }));
         };
 
@@ -155,9 +173,12 @@ export class ReservationsGridComponent implements OnInit {
         this.notOnePatronSelected = (rows: IdlObject[]) => (new Set(rows.map(row => row.usr().id())).size !== 1);
         this.notOneResourceSelected = (rows: IdlObject[]) => (new Set(rows.map(row => row.current_resource().id())).size !== 1);
         this.cancelNotAppropriate = (rows: IdlObject[]) => (this.noSelectedRows(rows) || ('pickedUp' === this.status));
+        this.pickupNotAppropriate = (rows: IdlObject[]) => (this.noSelectedRows(rows) || ('pickupReady' !== this.status));
         this.returnNotAppropriate = (rows: IdlObject[]) => {
             if (this.noSelectedRows(rows)) {
                 return true;
+            } else if (this.status && ('pickupReady' === this.status)) {
+                return true;
             } else {
                 rows.forEach(row => {
                     if ((null == row.pickup_time()) || row.return_time()) { return true; }
@@ -168,6 +189,15 @@ export class ReservationsGridComponent implements OnInit {
 
         this.reloadGrid = () => { this.grid.reload(); };
 
+        this.pickupSelected = (reservations: IdlObject[]) => {
+            const pickupOne = (thing: IdlObject) => {
+                if (!thing) { return; }
+                this.pickupResource(thing).subscribe(
+                    () => pickupOne(reservations.shift()));
+            };
+            pickupOne(reservations.shift());
+        };
+
         this.returnSelected = (reservations: IdlObject[]) => {
             const returnOne = (thing: IdlObject) => {
                 if (!thing) { return; }
@@ -177,6 +207,20 @@ export class ReservationsGridComponent implements OnInit {
             returnOne(reservations.shift());
         };
 
+        this.pickupResource = (reservation: IdlObject) => {
+            return this.net.request(
+               'open-ils.circ',
+               'open-ils.circ.reservation.pickup',
+               this.auth.token(),
+                   {'patron_barcode': reservation.usr().card().barcode(), 'reservation': reservation})
+               .pipe(tap(
+                   (success) => {
+                       this.onPickup.emit(reservation);
+                       this.grid.reload(); },
+                   (error) => { console.debug(error); }
+               ));
+        };
+
         this.returnResource = (reservation: IdlObject) => {
             return this.net.request(
                'open-ils.circ',
@@ -188,12 +232,37 @@ export class ReservationsGridComponent implements OnInit {
                    (error) => { console.debug(error); }
                ));
         };
+
         this.listReadOnlyFields = () => {
-            let list = "usr,xact_start,request_time,capture_time,pickup_time,return_time,capture_staff,target_resource_type,current_resource,target_resource,unrecovered,request_library,pickup_library,fine_interval,fine_amount,max_fine";
-            if (this.status) { list = list + ",start_time"; }
-            if ('returnedToday' === this.status) { list = list + ",end_time"; }
+            let list = 'usr,xact_start,request_time,capture_time,pickup_time,return_time,capture_staff,target_resource_type,' +
+                'current_resource,target_resource,unrecovered,request_library,pickup_library,fine_interval,fine_amount,max_fine';
+            if (this.status && ('pickupReady' !== this.status)) { list = list + ',start_time'; }
+            if (this.status && ('returnedToday' === this.status)) { list = list + ',end_time'; }
             return list;
-        }
+        };
+
+        this.startTimeShouldBeFuture = (fieldName: string, value: Moment, record: IdlObject) => {
+            if (Moment(value) < Moment()) {
+                return 'Start time must be in the future';
+            }
+            return '';
+        };
+
+        this.handleRowActivate = (row: IdlObject) => {
+            if (this.status) {
+                if ('returnReady' === this.status) {
+                    this.returnResource(row).subscribe();
+                } else if ('pickupReady' === this.status) {
+                    this.pickupResource(row).subscribe();
+                } else if ('returnedToday' === this.status) {
+                    this.toast.warning('Cannot edit this reservation');
+                } else {
+                    this.showEditDialog(row);
+                }
+            } else {
+                this.showEditDialog(row);
+            }
+        };
     }
 
     showEditDialog(idlThing: IdlObject) {
index 1743504..0e20a91 100644 (file)
@@ -3,8 +3,8 @@
 <eg-title i18n-prefix i18n-suffix prefix="Booking" suffix="Return"></eg-title>
 
 <!-- TODO: DRY This out: there has to be a good way to reuse the template -->
-<ngb-tabset (tabChange)="resetEverything()">
-  <ngb-tab title="By patron" i18n-title>
+<ngb-tabset (tabChange)="handleTabChange($event)" [activeId]="selectedTab">
+  <ngb-tab title="By patron" i18n-title id="patron">
     <ng-template ngbTabContent>
       <div class="row">
         <div class="col-md-4">
       </div>
       <div *ngIf="patronId">
         <h2 class="text-center" i18n>Ready for return</h2>
-        <eg-reservations-grid [patron]="patronId" status="pickedUp" persistSuffix="return.patron.picked_up"></eg-reservations-grid>
+        <eg-reservations-grid #patronReady [patron]="patronId" status="returnReady" persistSuffix="return.patron.picked_up"></eg-reservations-grid>
 
         <h2 class="text-center" i18n>Returned today</h2>
-        <eg-reservations-grid [patron]="patronId" status="returnedToday" persistSuffix="return.patron.returned"></eg-reservations-grid>
+        <eg-reservations-grid #patronReturned [patron]="patronId" status="returnedToday" persistSuffix="return.patron.returned"></eg-reservations-grid>
       </div>
     </ng-template>
   </ngb-tab>
-  <ngb-tab title="By resource" i18n-title>
+  <ngb-tab title="By resource" i18n-title id="resource">
     <ng-template ngbTabContent>
       <div class="input-group flex-nowrap">
         <div class="input-group-prepend">
       </div>
       <div *ngIf="patronId">
         <h2 class="text-center" i18n>Ready for return</h2>
-        <eg-reservations-grid [resource]="patronId" status="pickedUp" persistSuffix="return.patron.picked_up"></eg-reservations-grid>
+        <eg-reservations-grid #resourceReady [resource]="patronId" status="returnReady" persistSuffix="return.patron.picked_up"></eg-reservations-grid>
 
         <h2 class="text-center" i18n>Returned today</h2>
-        <eg-reservations-grid [patron]="patronId" status="returnedToday" persistSuffix="return.patron.returned"></eg-reservations-grid>
+        <eg-reservations-grid #resourceReturned [patron]="patronId" status="returnedToday" persistSuffix="return.patron.returned"></eg-reservations-grid>
       </div>
     </ng-template>
   </ngb-tab>
index 20d8b35..b00a672 100644 (file)
@@ -1,4 +1,8 @@
-import { Component, Input, OnInit } from '@angular/core';
+import { Component, Input, OnInit, ViewChild } from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import { NgbTabChangeEvent } from '@ng-bootstrap/ng-bootstrap';
+import {Observable} from 'rxjs';
+import {single} from 'rxjs/operators';
 import { GridDataSource } from '@eg/share/grid/grid';
 import { Pager } from '@eg/share/util/pager';
 import {PatronService} from '@eg/staff/share/patron.service';
@@ -6,8 +10,9 @@ import {PcrudService} from '@eg/core/pcrud.service';
 import {AuthService} from '@eg/core/auth.service';
 import {IdlObject} from '@eg/core/idl.service';
 import {NetService} from '@eg/core/net.service';
-import {Observable} from 'rxjs';
-import {single} from 'rxjs/operators';
+import {ReservationsGridComponent} from './reservations-grid.component';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {ToastService} from '@eg/share/toast/toast.service';
 
 
 @Component({
@@ -20,27 +25,61 @@ export class ReturnComponent implements OnInit {
     patronId: number;
     retrievePatronByBarcode: () => void;
     retrievePatronByResource: () => void;
+    selectedTab: 'patron' | 'resource' = 'patron';
 
     noSelectedRows: (rows: IdlObject[]) => boolean;
-    resetEverything: () => void;
+    handleTabChange: ($event: NgbTabChangeEvent) => void;
+    @ViewChild('patronReady') patronReady: ReservationsGridComponent;
+    @ViewChild('patronReturned') patronReturned: ReservationsGridComponent;
+    @ViewChild('resourceReady') resourceReady: ReservationsGridComponent;
+    @ViewChild('resourceReturned') resourceReturned: ReservationsGridComponent;
 
     constructor(
         private auth: AuthService,
         private net: NetService,
         private pcrud: PcrudService,
-        private patron: PatronService
+        private patron: PatronService,
+        private route: ActivatedRoute,
+        private router: Router,
+        private store: ServerStoreService,
+        private toast: ToastService
     ) {
     }
 
 
     ngOnInit() {
+        this.route.paramMap.subscribe((params: ParamMap) => {
+            this.patronId = +params.get('patron_id');
+            if (this.patronId) {
+                this.pcrud.search('au', {
+                    'id': this.patronId,
+                }, {
+                    limit: 1,
+                    flesh: 1,
+                    flesh_fields: {'au': ['card']}
+                }).subscribe(
+                    (resp) => {
+                        this.patronBarcode = resp.card().barcode();
+                        this.patronReady.reloadGrid();
+                        this.patronReturned.reloadGrid();
+                    }, (err) => { console.debug(err); }
+                );
+            } else {
+                this.store.getItem('eg.booking.return.tab').then(tab => {
+                    if (tab) { this.selectedTab = tab; }
+                });
+            }
+        });
+
         this.retrievePatronByBarcode = () => {
             if (this.patronBarcode) {
                 this.patron.bcSearch(this.patronBarcode).pipe(single()).subscribe(
-                    resp => { this.patronId = resp[0]['id']; }
+                    resp => { this.router.navigate(['/staff', 'booking', 'return', 'by_patron', resp[0].id]); },
+                    err => { this.toast.danger('No patron found with this barcode'); },
                 );
             }
         };
+
         this.retrievePatronByResource = () => {
             if (this.resourceBarcode) {
                 this.pcrud.search('brsrc', {'barcode': this.resourceBarcode}, {
@@ -50,15 +89,19 @@ export class ReturnComponent implements OnInit {
                     flesh_fields: {'brsrc': ['curr_rsrcs']},
                     select: {'curr_rsrcs': {'return_time': null, 'pickup_time': {'!=': null}}}
                 }).subscribe((resp) => {
-                    if (resp.curr_rsrcs().usr()) {
-                        this.patronId = resp.curr_rsrcs().usr();
+                    if (resp.curr_rsrcs()[0].usr()) {
+                        this.patronId = resp.curr_rsrcs()[0].usr();
+                        this.resourceReady.reloadGrid();
+                        this.resourceReturned.reloadGrid();
                     }
                 });
             }
         };
         this.noSelectedRows = (rows: IdlObject[]) => (rows.length === 0);
 
-        this.resetEverything = () => {
+        this.handleTabChange = ($event) => {
+            this.store.setItem('eg.booking.return.tab', $event.nextId);
+            this.router.navigate(['/staff', 'booking', 'return']);
             this.resourceBarcode = null;
             this.patronBarcode = null;
             this.patronId = null;
index b50d26c..a13375e 100644 (file)
@@ -26,8 +26,10 @@ const routes: Routes = [{
   component: PullListComponent
   }, {
   path: 'return',
-  component: ReturnComponent
-  },
+    children: [
+      {path: '', component: ReturnComponent},
+      {path: 'by_patron/:patron_id', component: ReturnComponent},
+  ]},
   ];
 
 @NgModule({
index 0aa6dae..0fab3aa 100644 (file)
             <span class="material-icons">trending_up</span>
             <span i18n>Pick Up Reservations</span>
           </a>
-          <a class="dropdown-item" href="/eg/staff/booking/legacy/booking/return">
+          <a class="dropdown-item" href="staff/booking/return">
             <span class="material-icons">trending_down</span>
             <span i18n>Return Reservations</span>
           </a>
index 02fd4d3..147fa93 100644 (file)
@@ -19921,6 +19921,12 @@ VALUES (
         'Sticky setting for filter tab in Manage Reservations',
         'cwst', 'label')
 ), (
+    'eg.booking.return.tab', 'gui', 'string',
+    oils_i18n_gettext(
+        'booking.return.tab',
+        'Sticky setting for tab in Booking Return',
+        'cwst', 'label')
+), (
     'eg.booking.pickup.ready.only_show_captured', 'gui', 'bool',
     oils_i18n_gettext(
         'booking.pickup.ready.only_show_captured',
index 2741f17..47555f4 100644 (file)
@@ -26,6 +26,12 @@ VALUES (
         'Sticky setting for filter tab in Manage Reservations',
         'cwst', 'label')
 ), (
+    'eg.booking.return.tab', 'gui', 'string',
+    oils_i18n_gettext(
+        'booking.return.tab',
+        'Sticky setting for tab in Booking Return',
+        'cwst', 'label')
+), (
     'eg.booking.pickup.ready.only_show_captured', 'gui', 'bool',
     oils_i18n_gettext(
         'booking.pickup.ready.only_show_captured',
index e151ede..50b9494 100644 (file)
@@ -224,7 +224,7 @@ angular.module('egCoreMod').run(['egStrings', function(s) {
             </a>
           </li>
           <li>
-            <a href="./booking/legacy/booking/return?patron_barcode={{patron().card().barcode()}}" target="_top">
+            <a href="/eg2/staff/booking/return/by_patron/{{patron().id()}}" target="_top">
               [% l('Booking: Return Reservations') %]
             </a>
           </li>
index 2e839b0..7ed79f1 100644 (file)
             </a>
           </li>
           <li>
-            <a href="./booking/legacy/booking/return" target="_self">
+            <a href="/eg2/staff/booking/return" target="_self">
               <span class="glyphicon glyphicon-import"></span>
               [% l('Return Reservations') %]
             </a>