LP1851306: Port Capture Booking Resource to Angular
authorJane Sandberg <sandbej@linnbenton.edu>
Sun, 17 Nov 2019 04:50:00 +0000 (20:50 -0800)
committerGalen Charlton <gmc@equinoxinitiative.org>
Thu, 18 Jun 2020 17:28:24 +0000 (13:28 -0400)
Signed-off-by: Jane Sandberg <sandbej@linnbenton.edu>
Signed-off-by: Terran McCanna <tmccanna@georgialibraries.org>
Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>
12 files changed:
Open-ILS/src/eg2/src/app/staff/booking/booking.module.ts
Open-ILS/src/eg2/src/app/staff/booking/capture.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/booking/capture.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/booking/reservation-actions.service.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/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.port_capture_reservations_to_angular.sql [new file with mode: 0644]
Open-ILS/src/templates/staff/navbar.tt2
docs/RELEASE_NOTES_NEXT/Circulation/angular-booking-capture.adoc [new file with mode: 0644]

index dbcfb03..bb3d1b1 100644 (file)
@@ -3,6 +3,7 @@ import {ReactiveFormsModule} from '@angular/forms';
 import {StaffCommonModule} from '@eg/staff/common.module';
 import {BookingRoutingModule} from './routing.module';
 import {CancelReservationDialogComponent} from './cancel-reservation-dialog.component';
+import {CaptureComponent} from './capture.component';
 import {CreateReservationComponent} from './create-reservation.component';
 import {CreateReservationDialogComponent} from './create-reservation-dialog.component';
 import {ManageReservationsComponent} from './manage-reservations.component';
@@ -28,6 +29,7 @@ import {OrgFamilySelectModule} from '@eg/share/org-family-select/org-family-sele
     ],
     declarations: [
         CancelReservationDialogComponent,
+        CaptureComponent,
         CreateReservationComponent,
         CreateReservationDialogComponent,
         ManageReservationsComponent,
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/capture.component.html b/Open-ILS/src/eg2/src/app/staff/booking/capture.component.html
new file mode 100644 (file)
index 0000000..b840f41
--- /dev/null
@@ -0,0 +1,22 @@
+<eg-string #noResourceString i18n-text text="No resource found with this barcode"></eg-string>
+<eg-string #captureSuccessString i18n-text text="Resource successfully captured"></eg-string>
+<eg-string #captureFailureString i18n-text text="Could not capture this resource"></eg-string>
+
+<eg-staff-banner bannerText="Booking Capture" i18n-bannerText>
+</eg-staff-banner>
+<eg-title i18n-prefix i18n-suffix prefix="Booking" suffix="Capture"></eg-title>
+
+<form [formGroup]="findResource" class="row">
+  <div class="col-md-4">
+    <div class="input-group flex-nowrap">
+      <div class="input-group-prepend">
+        <label class="input-group-text" for="resource-barcode" i18n>Resource barcode</label>
+        <input type="text" id="resource-barcode" class="form-control" formControlName="resourceBarcode">
+      </div>
+    </div>
+  </div>
+</form>
+
+<h2 class="text-center mt-2" i18n>Captured today</h2>
+<eg-reservations-grid #capturedTodayGrid status="capturedToday" persistSuffix="captured"></eg-reservations-grid>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/capture.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/capture.component.ts
new file mode 100644 (file)
index 0000000..5e4b7b3
--- /dev/null
@@ -0,0 +1,89 @@
+import {Component, OnInit, OnDestroy, ViewChild} from '@angular/core';
+import {FormGroup, FormControl} from '@angular/forms';
+import {of, Subscription} from 'rxjs';
+import {debounceTime, single, switchMap, tap} from 'rxjs/operators';
+import {AuthService} from '@eg/core/auth.service';
+import {NetService} from '@eg/core/net.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {BookingResourceBarcodeValidator} from './booking_resource_validator.directive';
+import {ReservationActionsService} from './reservation-actions.service';
+import {ReservationsGridComponent} from './reservations-grid.component';
+
+@Component({
+  templateUrl: './capture.component.html'
+})
+
+export class CaptureComponent implements OnInit, OnDestroy {
+
+    findResource: FormGroup;
+    subscriptions: Subscription[] = [];
+
+    @ViewChild('capturedTodayGrid', { static: false }) capturedTodayGrid: ReservationsGridComponent;
+    @ViewChild('noResourceString', { static: true }) noResourceString: StringComponent;
+    @ViewChild('captureSuccessString', { static: true }) captureSuccessString: StringComponent;
+    @ViewChild('captureFailureString', { static: true }) captureFailureString: StringComponent;
+
+    constructor(
+        private auth: AuthService,
+        private net: NetService,
+        private resourceValidator: BookingResourceBarcodeValidator,
+        private toast: ToastService,
+        private actions: ReservationActionsService
+    ) {
+    }
+
+    ngOnInit() {
+        this.findResource = new FormGroup({
+            'resourceBarcode': new FormControl(null, [], this.resourceValidator.validate)
+        });
+
+        const debouncing = 1500;
+        this.subscriptions.push(
+            this.resourceBarcode.valueChanges.pipe(
+                debounceTime(debouncing),
+                switchMap((val) => {
+                    if ('INVALID' === this.resourceBarcode.status) {
+                        this.noResourceString.current()
+                            .then(str => this.toast.danger(str));
+                        return of();
+                    } else {
+                        return this.net.request( 'open-ils.booking',
+                        'open-ils.booking.resources.capture_for_reservation',
+                        this.auth.token(), this.resourceBarcode.value )
+                        .pipe(switchMap((result: any) => {
+                            if (result && result.ilsevent !== undefined) {
+                                if (result.payload && result.payload.captured > 0) {
+                                    this.captureSuccessString.current()
+                                        .then(str => this.toast.success(str));
+                                    this.actions.printCaptureSlip(result.payload);
+                                    this.capturedTodayGrid.reloadGrid();
+                                } else {
+                                    this.captureFailureString.current()
+                                        .then(str => this.toast.danger(str));
+                                }
+                            } else {
+                                this.captureFailureString.current()
+                                    .then(str => this.toast.danger(str));
+                            }
+                            return of();
+                        }));
+                    }
+                })
+            )
+            .subscribe());
+
+    }
+
+    get resourceBarcode() {
+        return this.findResource.get('resourceBarcode');
+    }
+
+
+    ngOnDestroy(): void {
+        this.subscriptions.forEach((subscription) => {
+            subscription.unsubscribe();
+        });
+    }
+
+}
index 5545d06..79f5b33 100644 (file)
@@ -1,14 +1,33 @@
 import {Injectable} from '@angular/core';
 import {Router} from '@angular/router';
+import {Observable, of} from 'rxjs';
+import {mergeMap, switchMap, tap} from 'rxjs/operators';
+import {IdlObject} from '@eg/core/idl.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PrintService} from '@eg/share/print/print.service';
 import {PcrudService} from '@eg/core/pcrud.service';
 
 // Some grid actions that are shared across booking grids
 
+export interface CaptureInformation {
+    captured: number;
+    reservation: IdlObject;
+    mvr?: IdlObject;
+    new_copy_status?: number;
+    transit?: IdlObject;
+    resource?: IdlObject;
+    type?: IdlObject;
+    staff?: IdlObject;
+    workstation?: string;
+}
+
 @Injectable({providedIn: 'root'})
 export class ReservationActionsService {
 
     constructor(
+        private auth: AuthService,
         private pcrud: PcrudService,
+        private printer: PrintService,
         private router: Router,
     ) {
     }
@@ -17,6 +36,21 @@ export class ReservationActionsService {
         this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_resource', barcode]);
     }
 
+    printCaptureSlip = (templateData: CaptureInformation) => {
+        templateData.staff = this.auth.user();
+        templateData.workstation = this.auth.workstation();
+        this.printer.print({
+            templateName: 'booking_capture',
+            contextData: templateData,
+            printContext: 'receipt'
+        });
+    }
+
+    reprintCaptureSlip = (ids: number[]): Observable<CaptureInformation> => {
+        return this.fetchDataForCaptureSlip$(ids)
+        .pipe(tap((data) => this.printCaptureSlip(data)));
+    }
+
     viewItemStatus = (barcode: string) => {
         this.pcrud.search('acp', { 'barcode': barcode }, { limit: 1 })
         .subscribe((acp) => {
@@ -28,5 +62,34 @@ export class ReservationActionsService {
         return (new Set(ids).size !== 1);
     }
 
+    private fetchDataForCaptureSlip$ = (ids: number[]): Observable<CaptureInformation> => {
+        return this.pcrud.search('bresv', {'id': ids}, {
+            flesh: 2,
+            flesh_fields : {
+                'bresv': ['usr', 'current_resource', 'type'],
+                'au': ['card'],
+                'brsrc': ['type']
+            }
+        }).pipe(mergeMap((reservation: IdlObject) => this.assembleDataForCaptureSlip$(reservation)));
+    }
+
+    private assembleDataForCaptureSlip$ = (reservation: IdlObject): Observable<CaptureInformation> => {
+        let observable$ = of({
+            reservation: reservation,
+            captured: 1
+        });
+        if (reservation.pickup_lib() === this.auth.user().ws_ou()) {
+            observable$ = this.pcrud.search('artc', {'reservation': reservation.id()}, {limit: 1})
+            .pipe(switchMap(transit => {
+                return of({
+                    reservation: reservation,
+                    captured: 1,
+                    transit: transit
+                });
+            }));
+        }
+        return observable$;
+    }
+
 }
 
index b8b5b51..42f4b3e 100644 (file)
@@ -5,6 +5,7 @@
   persistKey="booking.{{persistSuffix}}" >
   <eg-grid-toolbar-action label="Edit Selected" i18n-label (onClick)="editSelected($event)" [disableOnRows]="editNotAppropriate"></eg-grid-toolbar-action>
   <eg-grid-toolbar-action label="Cancel Selected" i18n-label (onClick)="cancelSelected($event)" [disableOnRows]="cancelNotAppropriate"></eg-grid-toolbar-action>
+  <eg-grid-toolbar-action label="Reprint Capture Slip" i18n-label (onClick)="reprintCaptureSlip($event)" [disableOnRows]="reprintNotAppropriate"></eg-grid-toolbar-action>
   <eg-grid-toolbar-action label="Pick Up Selected" i18n-label (onClick)="pickupSelected($event)" [disableOnRows]="pickupNotAppropriate"></eg-grid-toolbar-action>
   <eg-grid-toolbar-action label="Return Selected" i18n-label (onClick)="returnSelected($event)" [disableOnRows]="returnNotAppropriate"></eg-grid-toolbar-action>
   <eg-grid-toolbar-action label="View Patron Record" i18n-label (onClick)="viewPatronRecord($event)" [disableOnRows]="notOnePatronSelected"></eg-grid-toolbar-action>
index 4e75a61..79caf7c 100644 (file)
@@ -31,7 +31,7 @@ export class ReservationsGridComponent implements OnChanges, OnInit {
     @Input() resourceBarcode: string;
     @Input() resourceType: number;
     @Input() pickupLibIds: number[];
-    @Input() status: 'pickupReady' | 'pickedUp' | 'returnReady' | 'returnedToday';
+    @Input() status: 'capturedToday' | 'pickupReady' | 'pickedUp' | 'returnReady' | 'returnedToday';
     @Input() persistSuffix: string;
     @Input() onlyCaptured = false;
 
@@ -51,6 +51,7 @@ export class ReservationsGridComponent implements OnChanges, OnInit {
     editSelected: (rows: IdlObject[]) => void;
     pickupSelected: (rows: IdlObject[]) => void;
     pickupResource: (rows: IdlObject) => Observable<any>;
+    reprintCaptureSlip: (rows: IdlObject[]) => void;
     returnSelected: (rows: IdlObject[]) => void;
     returnResource: (rows: IdlObject) => Observable<any>;
     cancelSelected: (rows: IdlObject[]) => void;
@@ -63,14 +64,13 @@ export class ReservationsGridComponent implements OnChanges, OnInit {
     handleRowActivate: (row: IdlObject) => void;
     redirectToCreate: () => void;
 
-    reloadGrid: () => void;
-
     noSelectedRows: (rows: IdlObject[]) => boolean;
     notOnePatronSelected: (rows: IdlObject[]) => boolean;
     notOneResourceSelected: (rows: IdlObject[]) => boolean;
     notOneCatalogedItemSelected: (rows: IdlObject[]) => boolean;
     cancelNotAppropriate: (rows: IdlObject[]) => boolean;
     pickupNotAppropriate: (rows: IdlObject[]) => boolean;
+    reprintNotAppropriate: (rows: IdlObject[]) => boolean;
     editNotAppropriate: (rows: IdlObject[]) => boolean;
     returnNotAppropriate: (rows: IdlObject[]) => boolean;
 
@@ -122,6 +122,9 @@ export class ReservationsGridComponent implements OnChanges, OnInit {
                     where['return_time'] = null;
                 } else if ('returnedToday' === this.status) {
                     where['return_time'] = {'>': Moment().startOf('day').toISOString()};
+                } else if ('capturedToday' === this.status) {
+                    where['capture_time'] = {'between': [Moment().startOf('day').toISOString(),
+                        Moment().add(1, 'day').startOf('day').toISOString()]};
                 }
             } else {
                 where['return_time'] = null;
@@ -185,12 +188,23 @@ export class ReservationsGridComponent implements OnChanges, OnInit {
         };
         this.cancelNotAppropriate = (rows: IdlObject[]) =>
             (this.noSelectedRows(rows) || ['pickedUp', 'returnReady', 'returnedToday'].includes(this.status));
-        this.pickupNotAppropriate = (rows: IdlObject[]) => (this.noSelectedRows(rows) || ('pickupReady' !== this.status));
+        this.pickupNotAppropriate = (rows: IdlObject[]) =>
+            (this.noSelectedRows(rows) || !('pickupReady' === this.status || 'capturedToday' === this.status));
         this.editNotAppropriate = (rows: IdlObject[]) => (this.noSelectedRows(rows) || ('returnedToday' === this.status));
+        this.reprintNotAppropriate = (rows: IdlObject[]) => {
+            if (this.noSelectedRows(rows)) {
+                return true;
+            } else if ('capturedToday' === this.status) {
+                return false;
+            } else if (rows.filter(row => !(row.capture_time())).length) { // If any of the rows have not been captured
+                return true;
+            }
+            return false;
+        };
         this.returnNotAppropriate = (rows: IdlObject[]) => {
             if (this.noSelectedRows(rows)) {
                 return true;
-            } else if (this.status && ('pickupReady' === this.status)) {
+            } else if (this.status && ('pickupReady' === this.status || 'capturedToday' === this.status)) {
                 return true;
             } else {
                 rows.forEach(row => {
@@ -200,8 +214,6 @@ export class ReservationsGridComponent implements OnChanges, OnInit {
             return false;
         };
 
-        this.reloadGrid = () => { this.grid.reload(); };
-
         this.pickupSelected = (reservations: IdlObject[]) => {
             const pickupOne = (thing: IdlObject) => {
                 if (!thing) { return; }
@@ -220,6 +232,10 @@ export class ReservationsGridComponent implements OnChanges, OnInit {
             returnOne(reservations.shift());
         };
 
+        this.reprintCaptureSlip = (reservations: IdlObject[]) => {
+            this.actions.reprintCaptureSlip(reservations.map((r) => r.id())).subscribe();
+        };
+
         this.pickupResource = (reservation: IdlObject) => {
             return this.net.request(
                'open-ils.circ',
@@ -278,6 +294,8 @@ export class ReservationsGridComponent implements OnChanges, OnInit {
 
     ngOnChanges() { this.reloadGrid(); }
 
+    reloadGrid() { this.grid.reload(); }
+
     enrichRow$ = (row: IdlObject): Observable<IdlObject> => {
         return from(this.org.settings('lib.timezone', row.pickup_lib().id())).pipe(
             switchMap((tz) => {
index bc12e96..75e34ba 100644 (file)
@@ -1,5 +1,6 @@
 import {NgModule} from '@angular/core';
 import {RouterModule, Routes} from '@angular/router';
+import {CaptureComponent} from './capture.component';
 import {CreateReservationComponent} from './create-reservation.component';
 import {ManageReservationsComponent} from './manage-reservations.component';
 import {PickupComponent} from './pickup.component';
@@ -20,6 +21,9 @@ const routes: Routes = [{
       {path: 'by_resource/:resource_barcode', component: ManageReservationsComponent},
       {path: 'by_resource_type/:resource_type_id', component: ManageReservationsComponent},
   ]}, {
+  path: 'capture',
+  component: CaptureComponent
+  }, {
   path: 'pickup',
     children: [
       {path: '', component: PickupComponent},
index 5310b5b..b6641b4 100644 (file)
             <span class="material-icons">list</span>
             <span i18n>Pull List</span>
           </a>
-          <a class="dropdown-item" href="/eg/staff/booking/legacy/booking/capture">
+          <a class="dropdown-item" routerLink="/staff/booking/capture">
             <span class="material-icons">pin_drop</span>
             <span i18n>Capture Resources</span>
           </a>
index b6959bb..6370049 100644 (file)
@@ -20280,6 +20280,42 @@ $TEMPLATE$
 $TEMPLATE$
 );
 
+INSERT INTO config.print_template
+    (id, name, locale, active, owner, label, template)
+VALUES (
+    3, 'booking_capture', 'en-US', TRUE,
+    (SELECT id FROM actor.org_unit WHERE parent_ou IS NULL),
+    oils_i18n_gettext(3, 'Booking capture slip', 'cpt', 'label'),
+$TEMPLATE$
+[%-
+    USE date;
+    SET data = template_data;
+    # template_data is data returned from open-ils.booking.resources.capture_for_reservation.
+-%]
+<div>
+  [% IF data.transit %]
+  <div>This item need to be routed to <strong>data.transit.dest</strong></div>
+  [% ELSE %]
+  <div>This item need to be routed to <strong>RESERVATION SHELF:</strong></div>
+  [% END %]
+  <div>Barcode: [% data.reservation.current_resource.barcode %]</div>
+  <div>Title: [% data.reservation.current_resource.type.name %]</div>
+  <div>Note: [% data.reservation.note %]</div>
+  <br/>
+  <p><strong>Reserved for patron</strong> [% data.reservation.usr.family_name %], [% data.reservation.usr.first_given_name %] [% data.reservation.usr.second_given_name %]
+  <br/>Barcode: [% data.reservation.usr.card.barcode %]</p>
+  <p>Request time: [% date.format(helpers.format_date(data.reservation.request_time, client_timezone), '%x %r', locale) %]
+  <br/>Reserved from:
+    [% date.format(helpers.format_date(data.reservation.start_time, client_timezone), '%x %r', locale) %]
+    - [% date.format(helpers.format_date(data.reservation.end_time, client_timezone), '%x %r', locale) %]</p>
+  <p>Slip date: [% date.format(helpers.current_date(client_timezone), '%x %r', locale) %]<br/>
+  Printed by [% data.staff.family_name %], [% data.staff.first_given_name %] [% data.staff.second_given_name %]
+    at [% data.workstation %]</p>
+</div>
+<br/>
+
+$TEMPLATE$
+);
 
 -- Allow for 1k stock templates
 SELECT SETVAL('config.print_template_id_seq'::TEXT, 1000);
@@ -20328,6 +20364,12 @@ VALUES (
         'Grid Config: Booking Return Resource tab Returned Today grid',
         'cwst', 'label')
 ), (
+    'eg.grid.booking.captured', 'gui', 'object',
+    oils_i18n_gettext(
+        'booking.manage',
+        'Grid Config: Booking Captured Reservations',
+        'cwst', 'label')
+), (
     'eg.booking.manage.selected_org_family', 'gui', 'object',
     oils_i18n_gettext(
         'booking.manage.selected_org_family',
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.port_capture_reservations_to_angular.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.port_capture_reservations_to_angular.sql
new file mode 100644 (file)
index 0000000..fbf53fc
--- /dev/null
@@ -0,0 +1,52 @@
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+INSERT INTO config.print_template 
+    (id, name, locale, active, owner, label, template) 
+VALUES (
+    3, 'booking_capture', 'en-US', TRUE,
+    (SELECT id FROM actor.org_unit WHERE parent_ou IS NULL),
+    oils_i18n_gettext(3, 'Booking capture slip', 'cpt', 'label'),
+$TEMPLATE$
+[%-
+    USE date;
+    SET data = template_data;
+    # template_data is data returned from open-ils.booking.resources.capture_for_reservation.
+-%]
+<div>
+  [% IF data.transit %]
+  <div>This item need to be routed to <strong>data.transit.dest</strong></div>
+  [% ELSE %]
+  <div>This item need to be routed to <strong>RESERVATION SHELF:</strong></div>
+  [% END %]
+  <div>Barcode: [% data.reservation.current_resource.barcode %]</div>
+  <div>Title: [% data.reservation.current_resource.type.name %]</div>
+  <div>Note: [% data.reservation.note %]</div>
+  <br/>
+  <p><strong>Reserved for patron</strong> [% data.reservation.usr.family_name %], [% data.reservation.usr.first_given_name %] [% data.reservation.usr.second_given_name %]
+  <br/>Barcode: [% data.reservation.usr.card.barcode %]</p>
+  <p>Request time: [% date.format(helpers.format_date(data.reservation.request_time, client_timezone), '%x %r', locale) %]
+  <br/>Reserved from:
+    [% date.format(helpers.format_date(data.reservation.start_time, client_timezone), '%x %r', locale) %]
+    - [% date.format(helpers.format_date(data.reservation.end_time, client_timezone), '%x %r', locale) %]</p>
+  <p>Slip date: [% date.format(helpers.current_date(client_timezone), '%x %r', locale) %]<br/>
+  Printed by [% data.staff.family_name %], [% data.staff.first_given_name %] [% data.staff.second_given_name %]
+    at [% data.workstation %]</p>
+</div>
+<br/>
+
+$TEMPLATE$
+);
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.grid.booking.captured', 'gui', 'object',
+    oils_i18n_gettext(
+        'booking.manage',
+        'Grid Config: Booking Captured Reservations',
+        'cwst', 'label')
+);
+
+
+COMMIT;
index 220c3d1..1a90182 100644 (file)
             </a>
           </li>
           <li>
-            <a href="./booking/legacy/booking/capture" target="_self">
+            <a href="/eg2/booking/capture" target="_self">
               <span class="glyphicon glyphicon-pushpin"></span>
               [% l('Capture Resources') %]
             </a>
diff --git a/docs/RELEASE_NOTES_NEXT/Circulation/angular-booking-capture.adoc b/docs/RELEASE_NOTES_NEXT/Circulation/angular-booking-capture.adoc
new file mode 100644 (file)
index 0000000..3d2aca7
--- /dev/null
@@ -0,0 +1,13 @@
+Booking Capture is now in Angular
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The interface to capture resources for booking
+reservations has been re-implemented in Angular.
+Other booking screens, such as Pick Up and
+Manage Reservations, now include an option to 
+re-print capture slips.
+
+System administrators can now edit the template
+for booking capture slips in Administration ->
+Server administration -> Print templates.
+