[open-ils-commits] [GIT] Evergreen ILS branch master updated. 472bdf0dc614f136be11e244a7672a5ca9a70aba

Evergreen Git git at git.evergreen-ils.org
Fri Sep 6 11:20:56 EDT 2019


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, master has been updated
       via  472bdf0dc614f136be11e244a7672a5ca9a70aba (commit)
       via  2b0acd70061a07a4091869a46c0ef2c8839f8165 (commit)
       via  1bc3d012b4ea77367e620665aa566640c013bd4c (commit)
       via  e00767dd1961aa757150518b8646d83d1660bf8e (commit)
       via  15014f4dd7fdb1fa1d3d826421aef7e8568f557e (commit)
       via  a388ce4f361887ae8a7252f7eda23379886fef6f (commit)
       via  d8f9c7bca3a3cbed614c6adda5a13361aa63f3c8 (commit)
       via  820386ec8b62e40bfe50eac9a68bd1d470a8233f (commit)
       via  5b5191464d777710dbff6c5a85f7c57b1135bbd5 (commit)
       via  bad7a0e8c38e23877608178f40583d52be6801e2 (commit)
       via  a4122fb0a593a974c9714d87bb9672333f73cf2f (commit)
       via  c8b1c86ffd2dfdd63a7c6f7375179a4229e6a4a6 (commit)
       via  19caf4b6b8d645cb93f0e29f6eaff51a7bc91e56 (commit)
       via  de4497f46097984573c808a36eef780eb35da1bd (commit)
      from  40b5853bd226041d991c0c356db2e6d016bf2410 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 472bdf0dc614f136be11e244a7672a5ca9a70aba
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Fri Sep 6 11:20:45 2019 -0400

    LP#1816475: stamp DB updates
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index 12db6e1c8b..0d567843ab 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -92,7 +92,7 @@ CREATE TRIGGER no_overlapping_deps
     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
 
-INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1175', :eg_version); -- gmcharlt/berick/miker/sandbergja
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1177', :eg_version); -- sandberja/cburns/gmcharlt
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.add_note_bresv.sql b/Open-ILS/src/sql/Pg/upgrade/1176.schema.add_note_bresv.sql
similarity index 53%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.schema.add_note_bresv.sql
rename to Open-ILS/src/sql/Pg/upgrade/1176.schema.add_note_bresv.sql
index 4742f1d9a4..a6e84ed09b 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.add_note_bresv.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1176.schema.add_note_bresv.sql
@@ -1,5 +1,7 @@
 BEGIN;
 
+SELECT evergreen.upgrade_deps_block_check('1176', :eg_version);
+
 ALTER TABLE booking.reservation
     ADD COLUMN note TEXT;
 
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.booking-sticky-settings.sql b/Open-ILS/src/sql/Pg/upgrade/1177.data.booking-sticky-settings.sql
similarity index 97%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.data.booking-sticky-settings.sql
rename to Open-ILS/src/sql/Pg/upgrade/1177.data.booking-sticky-settings.sql
index 8da02ed13b..55622d3dff 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.booking-sticky-settings.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1177.data.booking-sticky-settings.sql
@@ -1,5 +1,7 @@
 BEGIN;
---SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+SELECT evergreen.upgrade_deps_block_check('1177', :eg_version);
+
 INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
 VALUES (
     'eg.grid.booking.manage', 'gui', 'object',

commit 2b0acd70061a07a4091869a46c0ef2c8839f8165
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Fri Sep 6 11:17:56 2019 -0400

    LP#1816475: (follow-up) fix lint issues
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts
index 7461a6cfcb..6dcfe7c683 100644
--- a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts
@@ -274,7 +274,7 @@ export class CreateReservationComponent implements OnInit, AfterViewInit, OnDest
         resources$.pipe(
             tap((resource) =>  {
                 this.resources.push(resource);
-                this.resources.sort((a,b) =>
+                this.resources.sort((a, b) =>
                     (a.barcode() > b.barcode()) ? 1 : ((b.barcode() > a.barcode()) ? -1 : 0));
             }),
             takeLast(1),
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts
index 72b2d0458e..d4e39735b2 100644
--- a/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts
@@ -288,7 +288,7 @@ export class ReservationsGridComponent implements OnInit {
                 ok => {
                     this.toast.success('Reservation successfully updated'); // TODO: needs i18n, pluralization
                     this.grid.reload();
-                    resolve(ok)
+                    resolve(ok);
                 },
                 rejection => {}
             );
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.ts
index edde96cbc5..d818474d25 100644
--- a/Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.ts
@@ -871,7 +871,9 @@ export class HoldingsMaintenanceComponent implements OnInit {
     bookItems(rows: HoldingsEntry[]) {
         const copyIds = this.selectedCopyIds(rows);
         if (copyIds.length > 0) {
-            this.router.navigate(['staff', 'booking', 'create_reservation', 'for_resource', rows.filter(r => Boolean(r.copy))[0].copy.barcode()]);
+            this.router.navigate(
+                ['staff', 'booking', 'create_reservation', 'for_resource', rows.filter(r => Boolean(r.copy))[0].copy.barcode()]
+            );
         }
     }
 
@@ -886,7 +888,9 @@ export class HoldingsMaintenanceComponent implements OnInit {
     manageReservations(rows: HoldingsEntry[]) {
         const copyIds = this.selectedCopyIds(rows);
         if (copyIds.length > 0) {
-            this.router.navigate(['staff', 'booking', 'manage_reservations', 'by_resource', rows.filter(r => Boolean(r.copy))[0].copy.barcode()]);
+            this.router.navigate(
+                ['staff', 'booking', 'manage_reservations', 'by_resource', rows.filter(r => Boolean(r.copy))[0].copy.barcode()]
+            );
         }
     }
 }

commit 1bc3d012b4ea77367e620665aa566640c013bd4c
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Fri Sep 6 11:04:48 2019 -0400

    LP#1816475: (follow-up) ensure that manage reservations grid refreshes
    
    This applies the changes from LP#1823041 to make editing a record
    refresh the grid.
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts
index b14a8111e2..72b2d0458e 100644
--- a/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts
@@ -141,7 +141,7 @@ export class ReservationsGridComponent implements OnInit {
         this.editSelected = (idlThings: IdlObject[]) => {
             const editOneThing = (thing: IdlObject) => {
                 if (!thing) { return; }
-                this.showEditDialog(thing).subscribe(
+                this.showEditDialog(thing).then(
                     () => editOneThing(idlThings.shift()));
             };
            editOneThing(idlThings.shift()); };
@@ -283,12 +283,16 @@ export class ReservationsGridComponent implements OnInit {
     showEditDialog(idlThing: IdlObject) {
         this.editDialog.recId = idlThing.id();
         this.editDialog.timezone = idlThing['timezone'];
-        return this.editDialog.open({size: 'lg'}).pipe(tap(
-            () => {
-                this.toast.success('Reservation successfully updated'); // TODO: needs i18n, pluralization
-                this.grid.reload();
-            }
-        ));
+        return new Promise((resolve, reject) => {
+            this.editDialog.open({size: 'lg'}).subscribe(
+                ok => {
+                    this.toast.success('Reservation successfully updated'); // TODO: needs i18n, pluralization
+                    this.grid.reload();
+                    resolve(ok)
+                },
+                rejection => {}
+            );
+        });
     }
 
     filterByResourceBarcode(barcode: string) {

commit e00767dd1961aa757150518b8646d83d1660bf8e
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Fri Sep 6 10:44:28 2019 -0400

    LP#1816475: (follow-up) remove stray bare "ngModel"
    
    Including this in the field template for editing end_time
    had the effect of forcing the end date to be set to the
    current time instead of the current value of that field,
    which is unusual behavior. It also caused the form to
    have an automatically-invalid input state when editing
    a reservation with a start time that falls in the future.
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.html b/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.html
index 378e15bf41..a0579b5fc3 100644
--- a/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.html
@@ -58,7 +58,6 @@
 <ng-template #endTimeTemplate let-field="field" let-record="record">
   <eg-datetime-select
     domId="endTime"
-    ngModel
     [showTZ]="editDialog.timezone"
     [timezone]="editDialog.timezone"
     [egNotBeforeMoment]="momentizeIsoString(record['start_time'](), editDialog.timezone)"

commit 15014f4dd7fdb1fa1d3d826421aef7e8568f557e
Author: Jane Sandberg <sandbej at linnbenton.edu>
Date:   Mon Aug 26 15:18:34 2019 -0700

    LP1816475: Put the resource barcodes in order
    
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>
    Signed-off-by: Christine Burns <christine.burns at bc.libraries.coop>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts
index 1b25f41317..7461a6cfcb 100644
--- a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts
@@ -272,7 +272,11 @@ export class CreateReservationComponent implements OnInit, AfterViewInit, OnDest
         }
 
         resources$.pipe(
-            tap((resource) => this.resources.push(resource)),
+            tap((resource) =>  {
+                this.resources.push(resource);
+                this.resources.sort((a,b) =>
+                    (a.barcode() > b.barcode()) ? 1 : ((b.barcode() > a.barcode()) ? -1 : 0));
+            }),
             takeLast(1),
             switchMap(() => {
                 let range = {startTime: Moment(), endTime: Moment()};

commit a388ce4f361887ae8a7252f7eda23379886fef6f
Author: Jane Sandberg <sandbej at linnbenton.edu>
Date:   Tue Aug 20 14:27:31 2019 -0700

    LP1816475: Change "Pickup Library" terminology to "Reservation Location"
    
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>
    Signed-off-by: Christine Burns <christine.burns at bc.libraries.coop>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.html
index 243e4bf739..f5beb368f1 100644
--- a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.html
@@ -41,17 +41,18 @@
     </div>
     <div class="form-group row">
       <label class="col-lg-4 text-right font-weight-bold"
-        i18n for="create-pickup-library">Pickup library</label>
+        i18n for="create-pickup-library">Reservation location</label>
       <eg-org-select domId="create-pickup-library" [applyDefault]="true"
         [disableOrgs]="disableOrgs()" [hideOrgs]="disableOrgs()"
         (onChange)="handlePickupLibChange($event)">
       </eg-org-select>
+      <eg-help-popover helpText="The library where the resource is picked up or used" i18n-helpText></eg-help-popover>
     </div>
     <div *ngIf="pickupLibraryUsesDifferentTz"
       role="alert"
       class="alert alert-info">
       <span class="material-icons" aria-hidden="true">access_time</span>
-      <span i18n>Pickup Library is in the {{timezone}} timezone</span>
+      <span i18n>Reservation location is in the {{timezone}} timezone</span>
     </div>
     <div class="form-group row">
       <label class="col-lg-4 text-right font-weight-bold"
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/manage-reservations.component.html b/Open-ILS/src/eg2/src/app/staff/booking/manage-reservations.component.html
index b625672a1b..8fb6071fb5 100644
--- a/Open-ILS/src/eg2/src/app/staff/booking/manage-reservations.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/booking/manage-reservations.component.html
@@ -4,7 +4,7 @@
 
 <form [formGroup]="filters" class="row">
   <div class="col-sm-3">
-    <eg-org-family-select [hideAncestorSelector]="true" labelText="Pickup library" i18n-labelText formControlName="pickupLibraries">
+    <eg-org-family-select [hideAncestorSelector]="true" labelText="Reservation location" i18n-labelText formControlName="pickupLibraries">
     </eg-org-family-select>
   </div>
   <div class="col-sm-6 offset-sm-3">
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.html b/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.html
index ab8d923668..378e15bf41 100644
--- a/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.html
@@ -37,8 +37,8 @@
   <eg-grid-column i18n-label label="Resource Type" path="target_resource_type.name"></eg-grid-column>
   <eg-grid-column label="Reservation length" i18n-label  path="length"></eg-grid-column>
   <eg-grid-column label="Request library" i18n-label  path="request_lib.name"></eg-grid-column>
-  <eg-grid-column label="Pickup library" i18n-label path="pickup_lib.name"></eg-grid-column>
-  <eg-grid-column label="Pickup library timezone" i18n-label path="timezone"></eg-grid-column>
+  <eg-grid-column label="Reservation location" i18n-label path="pickup_lib.name"></eg-grid-column>
+  <eg-grid-column label="Reservation location timezone" i18n-label path="timezone"></eg-grid-column>
 
 </eg-grid>
 

commit d8f9c7bca3a3cbed614c6adda5a13361aa63f3c8
Author: Jane Sandberg <sandbej at linnbenton.edu>
Date:   Tue Aug 20 14:20:15 2019 -0700

    LP1816475: Fix circular dependency warning
    
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>
    Signed-off-by: Christine Burns <christine.burns at bc.libraries.coop>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/src/eg2/src/app/staff/booking/booking_resource_validator.directive.ts b/Open-ILS/src/eg2/src/app/staff/booking/booking_resource_validator.directive.ts
index 5e3fa712be..4509e139a6 100644
--- a/Open-ILS/src/eg2/src/app/staff/booking/booking_resource_validator.directive.ts
+++ b/Open-ILS/src/eg2/src/app/staff/booking/booking_resource_validator.directive.ts
@@ -3,9 +3,8 @@ import {NG_ASYNC_VALIDATORS, AsyncValidator, FormControl} from '@angular/forms';
 import {of} from 'rxjs';
 import {switchMap, catchError} from 'rxjs/operators';
 import {PcrudService} from '@eg/core/pcrud.service';
-import {BookingModule} from './booking.module';
 
- at Injectable({providedIn: BookingModule})
+ at Injectable({providedIn: 'root'})
 export class BookingResourceBarcodeValidator implements AsyncValidator {
     constructor(
         private pcrud: PcrudService) {
@@ -30,6 +29,7 @@ export class BookingResourceBarcodeValidator implements AsyncValidator {
         multi: true
     }]
 })
+
 export class BookingResourceBarcodeValidatorDirective {
     constructor(
         private validator: BookingResourceBarcodeValidator

commit 820386ec8b62e40bfe50eac9a68bd1d470a8233f
Author: Jane Sandberg <sandbej at linnbenton.edu>
Date:   Tue Aug 20 10:24:49 2019 -0700

    LP1816475: Changing icon for Manage Reservations
    
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>
    Signed-off-by: Christine Burns <christine.burns at bc.libraries.coop>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/src/eg2/src/app/staff/nav.component.html b/Open-ILS/src/eg2/src/app/staff/nav.component.html
index 2be451825e..d682541afb 100644
--- a/Open-ILS/src/eg2/src/app/staff/nav.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/nav.component.html
@@ -331,7 +331,7 @@
             <span i18n>Return Reservations</span>
           </a>
           <a class="dropdown-item" href="staff/booking/manage_reservations">
-            <span class="material-icons">edit_attributes</span>
+            <span class="material-icons">layers</span>
             <span i18n>Manage Reservations</span>
           </a>
         </div>

commit 5b5191464d777710dbff6c5a85f7c57b1135bbd5
Author: Jane Sandberg <sandbej at linnbenton.edu>
Date:   Tue Aug 20 10:07:17 2019 -0700

    LP1816475: Pre-fill patron barcode in Create Reservations
    
    Addresses comment #1 from
    https://bugs.launchpad.net/evergreen/+bug/1816475/comments/20
    
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>
    Signed-off-by: Christine Burns <christine.burns at bc.libraries.coop>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.html
index 1fe01953e2..243e4bf739 100644
--- a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.html
@@ -12,7 +12,7 @@
       <label class="col-lg-4 text-right font-weight-bold"
         i18n for="create-patron-barcode">Patron barcode</label>
       <input type="text" id="create-patron-barcode"
-        class="form-control col-lg-7" formControlName="patronBarcode">
+        class="form-control col-lg-7" formControlName="patronBarcode" [disabled]="patronId">
       <span class="col-lg-7 offset-lg-4" i18n>
         {{ (patron$ | async)?.first_given_name}}
         {{ (patron$ | async)?.second_given_name}}
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.ts
index 759c8e477d..96b2b6c3ae 100644
--- a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.ts
@@ -37,6 +37,7 @@ export class CreateReservationDialogComponent
     @Input() targetResource: number;
     @Input() targetResourceBarcode: string;
     @Input() targetResourceType: ComboboxEntry;
+    @Input() patronId: number;
     @Input() attributes: number[] = [];
     @Input() resources: IdlObject[] = [];
     @Output() onComplete: EventEmitter<boolean>;
@@ -83,6 +84,14 @@ export class CreateReservationDialogComponent
             'resourceList': new FormControl(),
         }, [startTimeIsBeforeEndTimeValidator]
         );
+        if (this.patronId) {
+            this.pcrud.search('au', {id: this.patronId}, {
+                flesh: 1,
+                flesh_fields: {'au': ['card']}
+            }).subscribe((usr) =>
+                this.create.patchValue({patronBarcode: usr.card().barcode()})
+            );
+        }
 
         this.addBresv$ = () => {
             let selectedResourceId = this.targetResource ? [this.targetResource] : null;
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.html b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.html
index ef67b08db9..64bbdf84df 100644
--- a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.html
@@ -182,6 +182,7 @@
 
 <eg-create-reservation-dialog #createDialog
   (onComplete)="fetchData()"
+  [patronId]="patronId"
   [targetResourceBarcode]="resourceBarcode"
   [targetResource]="resourceId"
   [targetResourceType]="resourceType.value"
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts
index 23cddcb3ff..1b25f41317 100644
--- a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts
@@ -44,7 +44,6 @@ export class CreateReservationComponent implements OnInit, AfterViewInit, OnDest
     multiday = false;
     resourceAvailabilityIcon: (row: ScheduleRow) => GridRowFlairEntry;
 
-    patronBarcode: string;
     patronId: number;
     resourceBarcode: string;
     resourceId: number;

commit bad7a0e8c38e23877608178f40583d52be6801e2
Author: Jane Sandberg <sandbej at linnbenton.edu>
Date:   Tue Aug 20 09:46:51 2019 -0700

    LP1816475: Loosening the permissions on booking resource type
    
    Addresses this issue: https://bugs.launchpad.net/evergreen/+bug/1816475/comments/19
    
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>
    Signed-off-by: Christine Burns <christine.burns at bc.libraries.coop>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index 6b1cf45df8..0dfe7831e7 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -5004,7 +5004,7 @@ SELECT  usr,
 		<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
 			<actions>
 				<create permission="ADMIN_BOOKING_RESOURCE_TYPE" context_field='owner'/>
-				<retrieve permission="ADMIN_BOOKING_RESOURCE_TYPE" context_field='owner'/>
+				<retrieve/>
 				<update permission="ADMIN_BOOKING_RESOURCE_TYPE" context_field='owner'/>
 				<delete permission="ADMIN_BOOKING_RESOURCE_TYPE" context_field='owner'/>
 			</actions>
@@ -5036,7 +5036,7 @@ SELECT  usr,
 		<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
 			<actions>
 				<create permission="ADMIN_BOOKING_RESOURCE" context_field='owner'/>
-				<retrieve permission="ADMIN_BOOKING_RESOURCE" context_field='owner'/>
+				<retrieve/>
 				<update permission="ADMIN_BOOKING_RESOURCE" context_field='owner'/>
 				<delete permission="ADMIN_BOOKING_RESOURCE" context_field='owner'/>
 			</actions>

commit a4122fb0a593a974c9714d87bb9672333f73cf2f
Author: Jane Sandberg <sandbej at linnbenton.edu>
Date:   Wed Aug 14 10:56:00 2019 -0700

    LP1816475: Removing development bits from the display
    
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>
    Signed-off-by: Christine Burns <christine.burns at bc.libraries.coop>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.html b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.html
index a658af65d4..ef67b08db9 100644
--- a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.html
@@ -1,8 +1,6 @@
 <eg-staff-banner bannerText="Create Reservation" i18n-bannerText>
 </eg-staff-banner>
 <eg-title i18n-prefix i18n-suffix prefix="Booking" suffix="Create Reservation"></eg-title>
-{{attributes | json}}
-{{selectedAttributes.value | json}}
 <form [formGroup]="criteria" class="row">
   <div class="col-sm-6">
     <div class="row">

commit c8b1c86ffd2dfdd63a7c6f7375179a4229e6a4a6
Author: Jane Sandberg <sandbej at linnbenton.edu>
Date:   Thu Jul 25 10:30:32 2019 -0700

    LP1816475: Docs: release notes and docs updates
    
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>
    Signed-off-by: Christine Burns <christine.burns at bc.libraries.coop>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/docs/RELEASE_NOTES_NEXT/Circulation/booking-refresh.adoc b/docs/RELEASE_NOTES_NEXT/Circulation/booking-refresh.adoc
new file mode 100644
index 0000000000..5e39e8debe
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Circulation/booking-refresh.adoc
@@ -0,0 +1,32 @@
+Booking Module Refresh
+^^^^^^^^^^^^^^^^^^^^^^
+
+The Booking module has been redesigned, with many of its interfaces being
+redesigned in Angular.
+
+This adds a new screen called "Manage Reservations", where staff can check details about
+all outstanding reservations, including those that have been recently placed, captured,
+picked up, or recently returned.
+
+On many screens within the new booking module, staff are able to edit reservations.  Previously,
+they would have needed to cancel and recreate those reservations with the new data.
+
+There is a new notes field attached to reservations, where staff can leave notes about the
+reservation.  One use case is to alert staff that a particular resource is being stored in
+an unfamiliar location.  This field is visible on all screens within the booking module.
+
+The Create Reservations UI is completely re-designed, and now includes a calendar-like view
+on which staff can view existing reservations and availability.
+
+Upgrade considerations
+++++++++++++++++++++++
+
+The Booking Module Refresh requires some new dependencies for the Angular
+client.  To install these, you will have to run the following commands:
+
+[source,bash]
+----
+cd $EVERGREEN_ROOT/Open-ILS/src/eg2/
+npm install
+----
+
diff --git a/docs/circulation/booking.adoc b/docs/circulation/booking.adoc
index 18a62ae1a2..230be514d4 100644
--- a/docs/circulation/booking.adoc
+++ b/docs/circulation/booking.adoc
@@ -4,101 +4,47 @@ Booking Module
 Creating a Booking Reservation
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Only staff members may create reservations. A reservation can be started from a patron record, or a booking resource. To reserve catalogued items, you may start from searching the catalogue, if you do not know the booking item's barcode.
-
-To create a reservation from a patron record
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-1) Retrieve the patron’s record.
-
-2) Select Other --> Booking --> Create or Cancel Reservations. This takes you to the Reservations Screen.
-
-image::media/booking-create-1_web_client.png[]
-
-3) For non-catalogued items, choose a Bookable Resource Type and click Next. For catalogued items, enter the barcode in Enter the barcode of a catalogued, bookable resource box, then click Next beside the box.
-
-image::media/booking-create-2_web_client.png[]
-
-4) For non-catalogued resources, the Bookable Resource Type and the items associated with the type will appear.
-
-image::media/booking-create-3_web_client.png[]
-
-For catalogued items, the title and the item will display in the box.
-
-5) Select the date and time for the reservation in *I need this resource...* area. Click the date field. A calendar widget will be displayed for you to choose a date. Click the time field to choose time from the dropdown list.
-
-image::media/booking-create-4_web_client.png[]
+indexterm:[scheduling,resources using the booking module]
+indexterm:[booking,reserving a resource]
+indexterm:[booking,creating a reservation]
+indexterm:[reserving a bookable resource]
 
 [NOTE]
-If incorrect date and time is selected, the date/time boxes will appear in red. For example, if the time for which the reservation is set has already passed, the boxes will appear in red. There must be at least 15 minutes between the creation of the reservation and the start time of the reservation.
-
-6) For non-catalogued resources, patrons may specify special feature(s), if any, of the resource. With these attributes: allows you to do so. For example, if a patron is booking a laptop he/she can choose between PC and Mac and even choose a specific operating system if they need to. Click the drop down arrow to select your option from the list.
-
-image::media/booking-create-5_web_client.png[]
-
-7) Select the pickup location from the dropdown list.
-
-image::media/booking-create-6_web_client.png[]
-
-8) If there are multiple copies of the resource and any item listed is acceptable, click Reserve Any. To choose a specific item, select it 
-and then click Reserve Selected.
-
-image::media/booking-create-7.png[]
-
-9) A message will confirm that the action succeeded. Click OK on the prompt.
-
-10) The screen will refresh and the reservation will appear below the patron’s name at the bottom of the screen.
-
-image::media/booking-create-9_web_client.png[]
-
-To create a reservation from a booking resource
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+The "Create a booking reservation" screen uses your library's timezone.  If you create a reservation at a library
+in a different timezone, Evergreen will alert you and provide the time in both your timezone and the other library's
+timezone.
 
-You need to know the barcode of the patron when you create a reservation for him/her from a booking resource.
+Only staff members may create reservations. A reservation can be started from a patron record, or a booking resource.
+To reserve catalogued items, you may start from searching the catalogue, if you do not know the booking item's barcode.
 
-1) From the Booking menu, select Create Reservations
-
-image::media/booking-create-module-1_web_client.png[]
-
-2) Choose a Bookable Resource Type and click Next or enter the barcode of a catalogued resource and click Next.
-
-image::media/booking-create-module-2.png[]
-
-3) For non-catalogued resources, a screen showing the Bookable Resource Type and the items associated with the type will appear.
-
-image::media/booking-create-module-3_web_client.png[]
-
-For catalogued resources, the title and item will appear.
-
-4) Enter the user’s barcode in the Reserve to patron barcode box. The user’s existing reservations, if any, will appear at the bottom of the screen.
-
-image::media/booking-create-module-4_web_client.png[]
-
-5) Select the date and time for the reservation in *I need this resource...* area. Click the date field. A calendar widget will be displayed for you to choose a date. Click the time field to choose time from the dropdown list.
-
-image::media/booking-create-4_web_client.png[]
-
-[NOTE]
-If incorrect date and time is selected, the date/time boxes will appear in red. For example, if the time for which the reservation is set has already passed, the boxes will appear in red. The times must be set correctly for the reservation to be created. There must be at least 15 minutes between the creation of the reservation and the start time of the reservation.
-
-
-6) For non-catalogued resources, patrons may specify special feature(s), if any, of the resource. The With these attributes: allows you to do so. For example, if a patron is booking a laptop they can choose between PC and Mac and even choose a specific operating system if they need to. Click the dropdown arrow to select your option from the list.
-
-image::media/booking-create-5_web_client.png[]
-
-7) Select the pickup location from the dropdown list.
-
-image::media/booking-create-6_web_client.png[]
-
-8) If there are multiple copies of the resource and any item listed is acceptable, click Reserve Any. To choose a specific item, select it and then click Reserve Selected.
-
-image::media/booking-create-7.png[]
-
-9) A message will confirm that the action succeeded. Click OK on the prompt.
-
-10) The screen will refresh and the reservation will appear below the patron’s name at the bottom of the screen.
+To create a reservation from a patron record
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-image::media/booking-create-9_web_client.png[]
+. Retrieve the patron's record.
+. Select Other -> Booking -> Create Reservations. This takes you to the Create Reservations Screen.
+. If you want to create a reservation that lasts less than a day (such as for a study room), select _Single-day reservation_
+as the reservation type.  If your reservation will last several days (such as for a video camera needed for a class project),
+select _Multiple-day reservation_.
+. In the area labeled "Reservation details", select the _Choose resource by barcode_ tab if you know the specific barcode
+of a resource you'd like to reserve.  Otherwise, select the _Choose resource by type_ tab.
+. A schedule grid  will display on the bottom part of the screen.
+. If necessary, adjust the day or days that are displayed.  You can also make other adjustments using the _Schedule settings_
+tab.
+. For non-catalogued resources, patrons may wish to specify certain attributes.  The _Attributes_ tab allows you to do this.
+For example, if a patron is booking a laptop, they can choose between PC and Mac laptops if they need to.
+. When you have found the days or times that work the best, you can proceed with creating the reservation, by doing one
+of the following:
+** Double click the appropriate row in the grid.
+** Use the tab and space keys to select the appropriate rows,
+then press Shift+F10 to open the actions menu.  Select
+"Create Reservation".
+** Select the appropriate rows in the grid, then right click
+to open the actions menu. Select "Create Reservation".
+** Select the appropriate rows in the grid, then select the
+actions button. Select "Create Reservation".
+. Adjust the values in this screen as necessary.
+. Select the "Confirm reservation" button.
+. The screen will refresh, and the new reservation will appear in the schedule.
 
 
 Search the catalogue to create a reservation
@@ -106,55 +52,62 @@ Search the catalogue to create a reservation
 
 If you would like to reserve a catalogued item but do not know the item barcode, you may start with a catalogue search.
 
-1) In the staff client, select Cataloguing --> Search the Catalogue or keyboard shortcut F3 to search for the item you wish to reserve. You may search by any bibliographic information.
-
-2) Click the title to display the record summary. In the Copy Summary, select Copy Details in Actions column.
-
-3) The Copy Details will appear in a new row. In the barcode column, click the book now link.
-
-4) A screen showing the title and barcodes of available copies will appear.
-
-5) Enter the user’s barcode in the Reserve to patron barcode box. The user’s existing reservations, if any, will appear at the bottom of the screen.
-
-6) Select the date and time in *I need this resource...* section. If the date and time set is incorrect the boxes appear in red. For example, if the time for which the reservation is set has already passed, the boxes will appear in red.
+. Select Cataloguing -> Search the Catalogue to search for the item you wish to reserve. You may search by any
+bibliographic information.
+. Select the _Holdings View_ tab.
+. Right-click on the row that you want to reserve. Select _Book Item Now_. This takes you to the Create Reservations Screen.
+. If you want to create a reservation that lasts less than a day (such as for a study room), select _Single-day reservation_
+as the reservation type.  If your reservation will last several days (such as for a video camera needed for a class project),
+select _Multiple-day reservation_.
+. A schedule grid  will display on the bottom part of the screen.
+. If necessary, adjust the day or days that are displayed.  You can also make other adjustments using the _Schedule settings_
+tab.
+. When you have found the days or times that work the best, you can proceed with creating the reservation, by doing one
+of the following:
+.* Double click the appropriate row in the grid.
+.* Use the tab and space keys to select the appropriate rows,
+then press Shift+F10 to open the actions menu.  Select
+"Create Reservation".
+.* Select the appropriate rows in the grid, then right click
+to open the actions menu. Select "Create Reservation".
+.* Select the appropriate rows in the grid, then select the
+actions button. Select "Create Reservation".
+. Enter the patron's barcode.
+. Adjust the values in this screen as necessary.
+. Select the "Confirm reservation" button.
+. The screen will refresh, and the new reservation will appear in the schedule.
 
-7) Select pickup location. If there are multiple copies and any of the listed items is acceptable, click Reserve Any. To choose a specific item, select it and then click Reserve Selected.
-
-8) A message will confirm that the action succeeded. Click OK on the prompt.
-
-9) The screen will refresh, and the reservation will appear below the user’s name.
 
 [NOTE]
-Reservations on catalogued items can be created on Item Status (F5) screen. Select the item, then Actions for Selected Items → Book Item Now.
+Reservations on catalogued items can be created on Item Status (F5) screen. Select the item, then Actions -> Book Item Now.
 
 Reservation Pull List
 ~~~~~~~~~~~~~~~~~~~~~
 
-Reservation pull list can be generated dynamically on the Staff Client.
-
-1) To create a pull list, select Booking --> Pull List.
-
-image::media/booking-pull-1_web_client.png[]
+indexterm:[booking,pull list]
+indexterm:[pull list,booking]
 
-2) You can decide how many days in advance you would like to pull reserved items. Enter the number of days in the box adjacent to Generate list for this many days hence. For example, if you would like to pull items that are needed today, you can enter 1 in the box, and you will retrieve items that need to be pulled today.
-
-3) Click Fetch to retrieve the pull list.
+Reservation pull list can be generated dynamically on the Staff Client.
 
-image::media/booking-pull-2.png[]
+. To create a pull list, select Booking -> Pull List.
 
-4) The pull list will appear. Click Print to print the pull list.
+. You can decide how many days in advance you would like to pull reserved items. Enter the number of days in the box
+adjacent to Generate list for this many days hence. For example, if you would like to pull items that are needed today,
+you can enter 1 in the box, and you will retrieve items that need to be pulled today.
 
-image::media/booking-pull-3.png[]
+. The pull list will appear. Select the actions button, then _Print_ to print the pull list.
 
 Capturing Items for Reservations
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Reservations must be captured before they are ready to be picked up by the patron.
+indexterm:[booking,capturing reservations]
+
+Depending on your library's workflow, reservations may need to be captured before they are ready to be picked up by the patron.
 
 [CAUTION]
 Always capture reservations in Booking Module. Check In function in Circulation does not function the same as Capture Resources.
 
-1) In the staff client, select Booking --> Capture Resources.
+1) In the staff client, select Booking -> Capture Resources.
 
 image::media/booking-capture-1_web_client.png[]
 
@@ -170,103 +123,67 @@ image::media/booking-capture-3.png[]
 Picking Up Reservations
 ~~~~~~~~~~~~~~~~~~~~~~~
 
-[CAUTION]
-Always use the dedicated Booking Module interfaces for tasks related to reservations. Items that have been captured for a reservation cannot be checked out using the Check Out interface, even if the patron is the reservation recipient.
-
-1) Ready-for-pickup reservations can be listed from Other --> Booking --> Pick Up Reservations within a patron record or Booking --> Pick Up Reservations.
-
-
-image::media/booking-pickup-1_web_client.png[]
-
-image::media/booking-pickup-module-1_web_client.png[]
+indexterm:[booking,picking up reservations]
+indexterm:[booking,checkout]
+indexterm:[checkout,booking resources]
 
+[CAUTION]
+Always use the dedicated Booking Module interfaces for tasks related to reservations. Items that have been captured for a
+reservation cannot be checked out using the Check Out interface, even if the patron is the reservation recipient.
 
-2) Scan the patron barcode if using Booking --> Pick Up Reservations.
+1) Ready-for-pickup reservations can be listed from Other -> Booking -> Pick Up Reservations within a patron record or Booking -> Pick Up Reservations.
 
-3) The reservation(s) available for pickup will display. Select those you want to pick up and click Pick Up.
+2) Scan the patron barcode if using Booking -> Pick Up Reservations.
 
-image::media/booking-pickup-2.png[]
+3) The reservation(s) available for pickup will display. Select those you want to pick up and double click them.
 
 4) The screen will refresh to show that the patron has picked up the reservation(s).
 
-image::media/booking-pickup-3.png[]
-
 
 Returning Reservations
 ~~~~~~~~~~~~~~~~~~~~~~
 
+indexterm:[booking,returning reservations]
+indexterm:[booking,checkin]
+indexterm:[checkin,booking resources]
+
 [CAUTION]
 When a reserved item is brought back, staff must use the Booking Module to return the reservation.
 
-1) To return reservations, select Booking --> Return Reservations
-
-image::media/booking-return-module-1.png[]
+1) To return reservations, select Booking -> Return Reservations
 
 2) You can return the item by patron or item barcode. Here we choose Resource to return by item barcode. Scan or enter the barcode, and click Go.
 
-image::media/booking-return-module-2.png[]
-
 3) A pop up box will tell you that the item was returned. Click OK on the prompt.
 
-4) If we select Patron on the above screen, after scanning the patron's barcode, reservations currently out to that patron are displayed. Highlight the reservations you want to return, and click Return.
-
-image::media/booking-return-2.png[]
+4) If we select Patron on the above screen, after scanning the patron's barcode, reservations currently out to that patron are displayed. Highlight the reservations you want to return, and double click them.
 
 5) The screen will refresh to show any resources that remain out and the reservations that have been returned.
 
-image::media/booking-return-module-4.png[]
-
 [NOTE]
-Reservations can be returned from within patron records by selecting Other --> Booking --> Return Reservations
+Reservations can be returned from within patron records by selecting Other -> Booking -> Return Reservations
 
 Cancelling a Reservation
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
-A reservation can be cancelled in a patron’s record or reservation creation screen.
+indexterm:[booking,canceling reservations]
+
+A reservation can be cancelled in a patron's record or reservation creation screen.
 
 Cancel a reservation from the patron record
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 1) Retrieve the patron's record.
 
-2) Select Other --> Booking --> Create or Cancel Reservations.
-
-image::media/booking-create-1_web_client.png[]
+2) Select Other -> Booking -> Manage Reservations.
 
 3) The existing reservations will appear at the bottom of the screen.
 
-image::media/booking-cancel-1.png[]
-
-4) Highlight the reservation that you want to cancel. Click Cancel Selected.
-
-image::media/booking-cancel-2.png[]
-
-[NOTE]
-Use Shift or Ctrl on keyboard and mouse click to select multiple reservations if needed.
+4) Highlight the reservation that you want to cancel. Select the Actions menu, then select _Cancel Selected_.
 
 5) A pop-up window will confirm the cancellation. Click OK on the prompt.
 
 6) The screen will refresh, and the cancelled reservation(s) will disappear.
 
-image::media/booking-cancel-4.png[]
-
-Cancel a reservation on reservation creation screen
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-1) Access the reservation creation screen by selecting Booking --> Create Reservations.
-
-2) Select any Bookable Resource Type, then click Next.
-
-3) Scan or type in the patron barcode in Reserve to Patron box then hit Enter.
-
-4) Patron's existing reservations will display at the bottom of the screen.
-
-5) Select those that you want to cancel, then click Cancel Selected.
-
-
-
-
-
-
 
 

commit 19caf4b6b8d645cb93f0e29f6eaff51a7bc91e56
Author: Jane Sandberg <sandbej at linnbenton.edu>
Date:   Thu Jul 25 10:28:47 2019 -0700

    LP1816475: Booking module refresh
    
    This commit ports several dojo interfaces to Angular(7).  As part of
    this work,
    * Adds moment.js-based timezone support to the Angular fmeditor and grid
    * Adds a note field to booking.reservation. This field is visible in all
    staff views of reservations (Create, Manage, Pull List, Capture, Pick Up
    and Return), but is not visible to the patron
    * Adds usrname as a selector for actor.usr
    * Adds the new booking.reservation note field to the receipt in the
    dojo-based Capture Reservations screen
    * Adds a read-only display of au to the fm-editor
    * Adds a new patron service in staff/share
    * Adds relevant workstation settings to the database
    * Adds form validation styles to reactive form fields
    * Adds a necessary polyfill for testing
    
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>
    Signed-off-by: Christine Burns <christine.burns at bc.libraries.coop>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index de6933f6f7..6b1cf45df8 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -3651,7 +3651,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 			<field reporter:label="Last Name" name="family_name"  reporter:datatype="text"/>
 			<field reporter:label="First Name" name="first_given_name"  reporter:datatype="text"/>
 			<field reporter:label="Home Library" name="home_ou" reporter:datatype="org_unit"/>
-			<field reporter:label="User ID" name="id" reporter:datatype="id" />
+			<field reporter:label="User ID" name="id" reporter:datatype="id" reporter:selector="usrname" />
 			<field reporter:label="Primary Identification Type" name="ident_type" reporter:datatype="link"/>
 			<field reporter:label="Secondary Identification Type" name="ident_type2" reporter:datatype="link"/>
 			<field reporter:label="Primary Identification" name="ident_value"  reporter:datatype="text"/>
@@ -5137,8 +5137,8 @@ SELECT  usr,
 			<field reporter:label="Payment Totals" name="payment_total" oils_persist:virtual="true" reporter:datatype="money"/>
 			<field reporter:label="Payment Summary" name="summary" oils_persist:virtual="true" reporter:datatype="link"/>
 			<field reporter:label="Request Time" name="request_time" reporter:datatype="timestamp"/>
-			<field reporter:label="Start Time" name="start_time" reporter:datatype="timestamp"/>
-			<field reporter:label="End Time" name="end_time" reporter:datatype="timestamp"/>
+			<field reporter:label="Start Time" name="start_time" reporter:datatype="timestamp" oils_obj:required="true"/>
+			<field reporter:label="End Time" name="end_time" reporter:datatype="timestamp" oils_obj:required="true"/>
 			<field reporter:label="Capture Time" name="capture_time" reporter:datatype="timestamp"/>
 			<field reporter:label="Cancel Time" name="cancel_time" reporter:datatype="timestamp"/>
 			<field reporter:label="Pickup Time" name="pickup_time" reporter:datatype="timestamp"/>
@@ -5154,6 +5154,7 @@ SELECT  usr,
 			<field reporter:label="Pickup Library" name="pickup_lib" reporter:datatype="link"/>
 			<field reporter:label="Capture Staff" name="capture_staff" reporter:datatype="link"/>
 			<field reporter:label="Notify by Email?" name="email_notify" reporter:datatype="bool"/>
+			<field reporter:label="Note" name="note" reporter:datatype="text"/>
 			<field reporter:label="Attribute Value Maps" name="attr_val_maps" oils_persist:virtual="true" reporter:datatype="link"/>
 		</fields>
 		<links>
diff --git a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html
index 85c0c65619..bb0e34785e 100644
--- a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html
+++ b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html
@@ -59,6 +59,18 @@
               </eg-date-select>
             </ng-container>
 
+            <ng-container *ngSwitchCase="'timestamp-timepicker'">
+              <eg-datetime-select
+                [showTZ]="timezone"
+                [timezone]="timezone"
+                domId="{{idPrefix}}-{{field.name}}"
+                (onChangeAsIso)="record[field.name]($event)"
+                i18n-validatorError
+                [readOnly]="field.readOnly"
+                initialIso="{{record[field.name]()}}">
+              </eg-datetime-select>
+            </ng-container>
+
             <ng-container *ngSwitchCase="'org_unit'">
               <eg-org-select
                 placeholder="{{field.label}}..."
@@ -134,6 +146,13 @@
                 (ngModelChange)="record[field.name]($event)"/>
             </ng-container>
   
+	    <ng-container *ngSwitchCase="'readonly-au'">
+              <ng-container *ngIf="field.linkedValues">
+                <a href="/eg/staff/circ/patron/{{field.linkedValues[0].id}}/checkout" target="_blank">{{field.linkedValues[0].label}}
+                <span class="material-icons" i18n-title title="Open user record in new tab">open_in_new</span></a>
+              </ng-container>
+            </ng-container>
+
             <ng-container *ngSwitchCase="'list'">
               <eg-combobox
                 id="{{idPrefix}}-{{field.name}}" name="{{field.name}}"
diff --git a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts
index b6e2638bbf..dcb085bcd7 100644
--- a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts
@@ -11,7 +11,7 @@ import {StringComponent} from '@eg/share/string/string.component';
 import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
 import {TranslateComponent} from '@eg/staff/share/translate/translate.component';
-
+import {FormatService} from '@eg/core/format.service';
 
 interface CustomFieldTemplate {
     template: TemplateRef<any>;
@@ -84,6 +84,9 @@ export class FmRecordEditorComponent
 
     recId: any;
 
+    // Show datetime fields in this particular timezone
+    timezone: string = this.format.wsOrgTimezone;
+
     // IDL record we are editing
     record: IdlObject;
 
@@ -108,6 +111,10 @@ export class FmRecordEditorComponent
     @Input() requiredFieldsList: string[] = [];
     @Input() requiredFields: string; // comma-separated string version
 
+    // list of timestamp fields that should display with a timepicker
+    @Input() datetimeFieldsList: string[] = [];
+    @Input() datetimeFields: string; // comma-separated string version
+
     // list of org_unit fields where a default value may be applied by
     // the org-select if no value is present.
     @Input() orgDefaultAllowedList: string[] = [];
@@ -169,6 +176,7 @@ export class FmRecordEditorComponent
       private idl: IdlService,
       private auth: AuthService,
       private toast: ToastService,
+      private format: FormatService,
       private pcrud: PcrudService) {
       super(modal);
     }
@@ -230,6 +238,9 @@ export class FmRecordEditorComponent
         if (this.requiredFields) {
             this.requiredFieldsList = this.requiredFields.split(/,/);
         }
+        if (this.datetimeFields) {
+            this.datetimeFieldsList = this.datetimeFields.split(/,/);
+        }
         if (this.orgDefaultAllowed) {
             this.orgDefaultAllowedList = this.orgDefaultAllowed.split(/,/);
         }
@@ -403,6 +414,8 @@ export class FmRecordEditorComponent
 
             promise = this.wireUpCombobox(field);
 
+        } else if (field.datatype === 'timestamp') {
+            field.datetime = this.datetimeFieldsList.includes(field.name);
         } else if (field.datatype === 'org_unit') {
             field.orgDefaultAllowed =
                 this.orgDefaultAllowedList.includes(field.name);
@@ -531,6 +544,10 @@ export class FmRecordEditorComponent
             return 'template';
         }
 
+        if ( field.datatype === 'timestamp' && field.datetime ) {
+            return 'timestamp-timepicker';
+        }
+
         // Some widgets handle readOnly for us.
         if (   field.datatype === 'timestamp'
             || field.datatype === 'org_unit'
@@ -543,6 +560,10 @@ export class FmRecordEditorComponent
                 return 'readonly-money';
             }
 
+            if (field.datatype === 'link' && field.class === 'au') {
+                return 'readonly-au';
+            }
+
             if (field.datatype === 'link' || field.linkedValues) {
                 return 'readonly-list';
             }
@@ -582,4 +603,3 @@ export class FmRecordEditorComponent
     }
 }
 
-
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts
index b616b82480..c612eb42fd 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts
@@ -33,6 +33,9 @@ export class GridColumnComponent implements OnInit {
     // Display date and time when datatype = timestamp
     @Input() datePlusTime: boolean;
 
+    // Display using a specific OU's timestamp when datatype = timestamp
+    @Input() timezoneContextOrg: number;
+
     // Used in conjunction with cellTemplate
     @Input() cellContext: any;
     @Input() cellTemplate: TemplateRef<any>;
@@ -65,6 +68,7 @@ export class GridColumnComponent implements OnInit {
         col.datatype = this.datatype;
         col.datePlusTime = this.datePlusTime;
         col.ternaryBool = this.ternaryBool;
+        col.timezoneContextOrg = this.timezoneContextOrg;
         col.isAuto = false;
         this.grid.context.columnSet.add(col);
     }
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html
index 55ca188f50..3b2237771b 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html
@@ -92,7 +92,7 @@
       title="Expand Cells Vertically" i18n-title
       class="material-icons mat-icon-in-button">expand_more</span>
     <span *ngIf="gridContext.overflowCells"
-      title="Collaps Cells Vertically" i18n-title
+      title="Collapse Cells Vertically" i18n-title
       class="material-icons mat-icon-in-button">expand_less</span>
   </button>
 
@@ -150,6 +150,3 @@
   </div>
 
 <div>
-
-
-
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid.component.html
index e29eb67e63..6301eec8c5 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid.component.html
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid.component.html
@@ -1,7 +1,7 @@
 
 <div class="eg-grid" role="grid">
 
-  <eg-grid-toolbar
+  <eg-grid-toolbar #toolbar
     [gridContext]="context"
     [gridPrinter]="gridPrinter"
     [colWidthConfig]="colWidthConfig"
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts
index 69edbf3130..29827bf234 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts
@@ -1,10 +1,11 @@
 import {Component, Input, Output, OnInit, AfterViewInit, EventEmitter,
-    OnDestroy, ViewEncapsulation} from '@angular/core';
+    OnDestroy, ViewChild, ViewEncapsulation} from '@angular/core';
 import {IdlService} from '@eg/core/idl.service';
 import {OrgService} from '@eg/core/org.service';
 import {ServerStoreService} from '@eg/core/server-store.service';
 import {FormatService} from '@eg/core/format.service';
 import {GridContext, GridColumn, GridDataSource, GridRowFlairEntry} from './grid';
+import {GridToolbarComponent} from './grid-toolbar.component';
 
 /**
  * Main grid entry point.
@@ -125,6 +126,8 @@ export class GridComponent implements OnInit, AfterViewInit, OnDestroy {
     @Output() onRowActivate: EventEmitter<any>;
     @Output() onRowClick: EventEmitter<any>;
 
+    @ViewChild('toolbar') toolbar: GridToolbarComponent;
+
     constructor(
         private idl: IdlService,
         private org: OrgService,
@@ -190,6 +193,10 @@ export class GridComponent implements OnInit, AfterViewInit, OnDestroy {
         this.context.destroy();
     }
 
+    print = () => {
+        this.toolbar.printHtml();
+    }
+
     reload() {
         this.context.reload();
     }
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.ts
index 7835f454ab..01b5c09aa8 100644
--- a/Open-ILS/src/eg2/src/app/share/grid/grid.ts
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid.ts
@@ -28,6 +28,7 @@ export class GridColumn {
     datatype: string;
     datePlusTime: boolean;
     ternaryBool: boolean;
+    timezoneContextOrg: number;
     cellTemplate: TemplateRef<any>;
     cellContext: any;
     isIndex: boolean;
@@ -732,7 +733,8 @@ export class GridContext {
             idlClass: col.idlClass,
             idlField: col.idlFieldDef ? col.idlFieldDef.name : col.name,
             datatype: col.datatype,
-            datePlusTime: Boolean(col.datePlusTime)
+            datePlusTime: Boolean(col.datePlusTime),
+            timezoneContextOrg: Number(col.timezoneContextOrg)
         });
     }
 
diff --git a/Open-ILS/src/eg2/src/app/share/validators/not_before_moment_validator.directive.ts b/Open-ILS/src/eg2/src/app/share/validators/not_before_moment_validator.directive.ts
new file mode 100644
index 0000000000..7b6f1fc55d
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/validators/not_before_moment_validator.directive.ts
@@ -0,0 +1,32 @@
+import {Directive, Input} from '@angular/core';
+import {NG_VALIDATORS, AbstractControl, FormControl, ValidationErrors, ValidatorFn} from '@angular/forms';
+import {Injectable} from '@angular/core';
+
+import * as Moment from 'moment-timezone';
+
+export function notBeforeMomentValidator(notBeforeMe: Moment): ValidatorFn {
+    return (control: AbstractControl): {[key: string]: any} | null => {
+        return (control.value && control.value.isBefore(notBeforeMe)) ?
+            {tooEarly: 'This cannot be before ' + notBeforeMe.format('LLL')} : null;
+    };
+}
+
+ at Directive({
+    selector: '[egNotBeforeMoment]',
+    providers: [{
+        provide: NG_VALIDATORS,
+        useExisting: NotBeforeMomentValidatorDirective,
+        multi: true
+    }]
+})
+export class NotBeforeMomentValidatorDirective {
+    @Input('egNotBeforeMoment') egNotBeforeMoment: Moment;
+
+    validate(control: AbstractControl): {[key: string]: any} | null {
+        return this.egNotBeforeMoment ?
+            notBeforeMomentValidator(this.egNotBeforeMoment)(control)
+            : null;
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/validators/patron_barcode_validator.directive.spec.ts b/Open-ILS/src/eg2/src/app/share/validators/patron_barcode_validator.directive.spec.ts
new file mode 100644
index 0000000000..1e1208e0cf
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/validators/patron_barcode_validator.directive.spec.ts
@@ -0,0 +1,43 @@
+import {PatronBarcodeValidator} from './patron_barcode_validator.directive';
+import {of} from 'rxjs';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {EventService} from '@eg/core/event.service';
+import {StoreService} from '@eg/core/store.service';
+
+let netService: NetService;
+let authService: AuthService;
+let evtService: EventService;
+let storeService: StoreService;
+
+beforeEach(() => {
+    evtService = new EventService();
+    storeService = new StoreService(null /* CookieService */);
+    netService = new NetService(evtService);
+    authService = new AuthService(evtService, netService, storeService);
+});
+
+describe('PatronBarcodeValidator', () => {
+    it('should not throw an error if there is exactly 1 match', () => {
+        const pbv = new PatronBarcodeValidator(authService, netService);
+        pbv['parseActorCall'](of(1))
+        .subscribe((val) => {
+            expect(val).toBeNull();
+        });
+    });
+    it('should throw an error if there is more than 1 match', () => {
+        const pbv = new PatronBarcodeValidator(authService, netService);
+        pbv['parseActorCall'](of(1, 2, 3))
+        .subscribe((val) => {
+            expect(val).not.toBeNull();
+        });
+    });
+    it('should throw an error if there is no match', () => {
+        const pbv = new PatronBarcodeValidator(authService, netService);
+        pbv['parseActorCall'](of())
+        .subscribe((val) => {
+            expect(val).not.toBeNull();
+        });
+    });
+});
+
diff --git a/Open-ILS/src/eg2/src/app/share/validators/patron_barcode_validator.directive.ts b/Open-ILS/src/eg2/src/app/share/validators/patron_barcode_validator.directive.ts
new file mode 100644
index 0000000000..81d1b159b0
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/validators/patron_barcode_validator.directive.ts
@@ -0,0 +1,56 @@
+import { Directive, forwardRef } from '@angular/core';
+import { NG_VALIDATORS, NG_ASYNC_VALIDATORS, AbstractControl, ValidationErrors, AsyncValidator, FormControl } from '@angular/forms';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {EmptyError, Observable, of} from 'rxjs';
+import {single, switchMap, catchError} from 'rxjs/operators';
+import {Injectable} from '@angular/core';
+
+ at Injectable({providedIn: 'root'})
+export class PatronBarcodeValidator implements AsyncValidator {
+    constructor(
+        private auth: AuthService,
+        private net: NetService) {
+    }
+
+    validate = (control: FormControl) => {
+        return this.parseActorCall(this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.get_barcodes',
+            this.auth.token(),
+            this.auth.user().ws_ou(),
+            'actor', control.value));
+    }
+
+    private parseActorCall = (actorCall: Observable<any>) => {
+        return actorCall
+        .pipe(single(),
+        switchMap(() => of(null)),
+        catchError((err) => {
+            if (err instanceof EmptyError) {
+                return of({ patronBarcode: 'No patron found with that barcode' });
+            } else if ('Sequence contains more than one element' === err) {
+                return of({ patronBarcode: 'Barcode matches more than one patron' });
+            }
+        }));
+    }
+}
+
+ at Directive({
+    selector: '[egValidPatronBarcode]',
+    providers: [{
+        provide: NG_ASYNC_VALIDATORS,
+        useExisting: forwardRef(() => PatronBarcodeValidator),
+        multi: true
+    }]
+})
+export class PatronBarcodeValidatorDirective {
+    constructor(
+        private pbv: PatronBarcodeValidator
+    ) { }
+
+    validate = (control: FormControl) => {
+        this.pbv.validate(control);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/booking.module.ts b/Open-ILS/src/eg2/src/app/staff/booking/booking.module.ts
new file mode 100644
index 0000000000..65da637f14
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/booking.module.ts
@@ -0,0 +1,42 @@
+import {NgModule} from '@angular/core';
+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 {CreateReservationComponent} from './create-reservation.component';
+import {CreateReservationDialogComponent} from './create-reservation-dialog.component';
+import {ManageReservationsComponent} from './manage-reservations.component';
+import {ReservationsGridComponent} from './reservations-grid.component';
+import {PickupComponent} from './pickup.component';
+import {PullListComponent} from './pull-list.component';
+import {ReturnComponent} from './return.component';
+import {NoTimezoneSetComponent} from './no-timezone-set.component';
+import {PatronService} from '@eg/staff/share/patron.service';
+import {BookingResourceBarcodeValidatorDirective} from './booking_resource_validator.directive';
+
+
+ at NgModule({
+    imports: [
+        StaffCommonModule,
+        BookingRoutingModule,
+        ReactiveFormsModule,
+    ],
+    providers: [PatronService],
+    declarations: [
+        CancelReservationDialogComponent,
+        CreateReservationComponent,
+        CreateReservationDialogComponent,
+        ManageReservationsComponent,
+        NoTimezoneSetComponent,
+        PickupComponent,
+        PullListComponent,
+        ReservationsGridComponent,
+        ReturnComponent,
+        BookingResourceBarcodeValidatorDirective
+    ],
+    exports: [
+        BookingResourceBarcodeValidatorDirective
+    ]
+})
+export class BookingModule { }
+
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/booking_resource_validator.directive.ts b/Open-ILS/src/eg2/src/app/staff/booking/booking_resource_validator.directive.ts
new file mode 100644
index 0000000000..5e3fa712be
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/booking_resource_validator.directive.ts
@@ -0,0 +1,42 @@
+import {Directive, forwardRef, Injectable} from '@angular/core';
+import {NG_ASYNC_VALIDATORS, AsyncValidator, FormControl} from '@angular/forms';
+import {of} from 'rxjs';
+import {switchMap, catchError} from 'rxjs/operators';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {BookingModule} from './booking.module';
+
+ at Injectable({providedIn: BookingModule})
+export class BookingResourceBarcodeValidator implements AsyncValidator {
+    constructor(
+        private pcrud: PcrudService) {
+    }
+
+    validate = (control: FormControl) => {
+        return this.pcrud.search('brsrc',
+            {'barcode' : control.value},
+            {'limit': 1}).pipe(
+                switchMap(() => of(null)),
+                catchError((err) => {
+                    return of({ resourceBarcode: 'No resource found with that barcode' });
+                }));
+    }
+}
+
+ at Directive({
+    selector: '[egValidBookingResourceBarcode]',
+    providers: [{
+        provide: NG_ASYNC_VALIDATORS,
+        useExisting: forwardRef(() => BookingResourceBarcodeValidator),
+        multi: true
+    }]
+})
+export class BookingResourceBarcodeValidatorDirective {
+    constructor(
+        private validator: BookingResourceBarcodeValidator
+    ) { }
+
+    validate = (control: FormControl) => {
+        this.validator.validate(control);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/cancel-reservation-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/cancel-reservation-dialog.component.ts
new file mode 100644
index 0000000000..022ef96a38
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/cancel-reservation-dialog.component.ts
@@ -0,0 +1,63 @@
+import {Component, EventEmitter, Output, ViewChild} from '@angular/core';
+import {switchMap} from 'rxjs/operators';
+import {AuthService} from '@eg/core/auth.service';
+import {NetService} from '@eg/core/net.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+
+ at Component({
+  selector: 'eg-cancel-reservation-dialog',
+  template: `
+  <eg-confirm-dialog #confirmCancelReservationDialog
+    i18n-dialogTitle i18n-dialogBody
+    dialogTitle="Confirm Cancelation"
+    [dialogBodyTemplate]="confirmMessage">
+  </eg-confirm-dialog>
+  <ng-template #confirmMessage>
+    <span i18n>
+      Are you sure you want to cancel
+      {reservations.length, plural, =1 {this reservation} other {these {{reservations.length}} reservations}}?
+    </span>
+  </ng-template>
+  `
+})
+
+export class CancelReservationDialogComponent {
+
+    constructor(
+        private auth: AuthService,
+        private net: NetService,
+        private toast: ToastService
+    ) {
+    }
+
+    reservations: number[];
+
+    @ViewChild('confirmCancelReservationDialog')
+        private cancelReservationDialog: ConfirmDialogComponent;
+
+    @Output() onSuccessfulCancel = new EventEmitter();
+
+    open(reservations: number[]) {
+        this.reservations = reservations;
+        this.cancelReservationDialog.open()
+            .pipe(
+                switchMap(() => this.net.request(
+                    'open-ils.booking',
+                    'open-ils.booking.reservations.cancel',
+                    this.auth.token(), reservations))
+            )
+            .subscribe(
+                (res) => {
+                    if (res.textcode) {
+                        this.toast.danger('Could not cancel reservation'); // TODO: needs i18n, pluralization
+                    } else {
+                        this.toast.success('Reservation successfully canceled'); // TODO: needs i18n, pluralization
+                        this.onSuccessfulCancel.emit();
+                    }
+                }
+            );
+    }
+
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.html
new file mode 100644
index 0000000000..1fe01953e2
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.html
@@ -0,0 +1,86 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h3 class="modal-title" i18n>Confirm Reservation Details</h3>
+    <button type="button" class="close"
+      i18n-aria-label aria-label="Close"
+      (click)="dismiss('cross_click')">
+      <span aria-hidden="true">×</span>
+    </button>
+  </div>
+  <form class="modal-body form-common" [formGroup]="create">
+    <div class="form-group row">
+      <label class="col-lg-4 text-right font-weight-bold"
+        i18n for="create-patron-barcode">Patron barcode</label>
+      <input type="text" id="create-patron-barcode"
+        class="form-control col-lg-7" formControlName="patronBarcode">
+      <span class="col-lg-7 offset-lg-4" i18n>
+        {{ (patron$ | async)?.first_given_name}}
+        {{ (patron$ | async)?.second_given_name}}
+        {{ (patron$ | async)?.family_name}}
+      </span>
+    </div>
+    <div class="form-group row">
+      <label class="col-lg-4 text-right font-weight-bold"
+        i18n for="create-end-time">Start time</label>
+      <eg-datetime-select
+        formControlName="startTime"
+        [timezone]="timezone">
+      </eg-datetime-select>
+    </div>
+    <div class="form-group row">
+      <label class="col-lg-4 text-right font-weight-bold"
+        i18n for="create-end-time">End time</label>
+      <eg-datetime-select
+        formControlName="endTime"
+        [timezone]="timezone">
+      </eg-datetime-select>
+      <div role="alert" class="alert alert-danger" *ngIf="create.errors && create.errors.startTimeNotBeforeEndTime">
+        <span class="material-icons" aria-hidden="true">error</span>
+        <span i18n>Start time must be before end time</span>
+      </div>
+    </div>
+    <div class="form-group row">
+      <label class="col-lg-4 text-right font-weight-bold"
+        i18n for="create-pickup-library">Pickup library</label>
+      <eg-org-select domId="create-pickup-library" [applyDefault]="true"
+        [disableOrgs]="disableOrgs()" [hideOrgs]="disableOrgs()"
+        (onChange)="handlePickupLibChange($event)">
+      </eg-org-select>
+    </div>
+    <div *ngIf="pickupLibraryUsesDifferentTz"
+      role="alert"
+      class="alert alert-info">
+      <span class="material-icons" aria-hidden="true">access_time</span>
+      <span i18n>Pickup Library is in the {{timezone}} timezone</span>
+    </div>
+    <div class="form-group row">
+      <label class="col-lg-4 text-right font-weight-bold"
+        i18n for="create-resource">Resource</label>
+      <input *ngIf="targetResource && targetResourceBarcode" id="create-resource" value="{{targetResourceBarcode}}" disabled>
+      <eg-combobox
+        formControlName="resourceList"
+        *ngIf="!(targetResource && targetResourceBarcode)"
+        startId="any">
+        <eg-combobox-entry entryId="any" entryLabel="Any resource"
+          i18n-entryLabel></eg-combobox-entry>
+        <eg-combobox-entry *ngFor="let r of resources" entryId="{{r.id()}}" entryLabel="{{r.barcode()}}">
+        </eg-combobox-entry>
+      </eg-combobox>
+    </div>
+    <div class="form-group row">
+      <label class="col-lg-4 text-right font-weight-bold"
+        i18n for="create-email-notify">Notify by email?</label>
+      <input type="checkbox" formControlName="emailNotify">
+    </div>
+  </form>
+  <div class="modal-footer">
+    <button (click)="addBresv$().subscribe()" [disabled]="!create.valid" class="btn btn-info" i18n>Confirm reservation</button>
+    <button (click)="addBresvAndOpenPatronReservations()" [disabled]="!create.valid" class="btn btn-info" i18n>
+      Confirm and show patron reservations
+    </button>
+    <button (click)="close()" class="btn btn-warning ml-2" i18n>Cancel</button>
+  </div>
+</ng-template>
+<eg-alert-dialog #fail i18n-dialogBody
+  dialogBody="Could not create this reservation">
+</eg-alert-dialog>
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.ts
new file mode 100644
index 0000000000..759c8e477d
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.ts
@@ -0,0 +1,203 @@
+import {Component, Input, Output, OnInit, ViewChild, EventEmitter} from '@angular/core';
+import {FormGroup, FormControl, Validators, ValidatorFn, ValidationErrors} from '@angular/forms';
+import {Router} from '@angular/router';
+import {Observable, of} from 'rxjs';
+import {switchMap, single, startWith, tap} from 'rxjs/operators';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {AuthService} from '@eg/core/auth.service';
+import {FormatService} from '@eg/core/format.service';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {PatronBarcodeValidator} from '@eg/share/validators/patron_barcode_validator.directive';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import * as Moment from 'moment-timezone';
+
+const startTimeIsBeforeEndTimeValidator: ValidatorFn = (fg: FormGroup): ValidationErrors | null => {
+    const start = fg.get('startTime').value;
+    const end = fg.get('endTime').value;
+    return start !== null && end !== null &&
+        start.isBefore(end)
+        ? null
+        : { startTimeNotBeforeEndTime: true };
+};
+
+ at Component({
+  selector: 'eg-create-reservation-dialog',
+  templateUrl: './create-reservation-dialog.component.html'
+})
+
+export class CreateReservationDialogComponent
+    extends DialogComponent implements OnInit {
+
+    @Input() targetResource: number;
+    @Input() targetResourceBarcode: string;
+    @Input() targetResourceType: ComboboxEntry;
+    @Input() attributes: number[] = [];
+    @Input() resources: IdlObject[] = [];
+    @Output() onComplete: EventEmitter<boolean>;
+
+    create: FormGroup;
+    patron$: Observable<{first_given_name: string, second_given_name: string, family_name: string}>;
+    pickupLibId: number;
+    timezone: string = this.format.wsOrgTimezone;
+    pickupLibraryUsesDifferentTz: boolean;
+
+    public disableOrgs: () => number[];
+    addBresv$: () => Observable<any>;
+    @ViewChild('fail') private fail: AlertDialogComponent;
+
+    handlePickupLibChange: ($event: IdlObject) => void;
+
+    constructor(
+        private auth: AuthService,
+        private format: FormatService,
+        private net: NetService,
+        private org: OrgService,
+        private pcrud: PcrudService,
+        private router: Router,
+        private modal: NgbModal,
+        private pbv: PatronBarcodeValidator,
+        private toast: ToastService
+    ) {
+        super(modal);
+        this.onComplete = new EventEmitter<boolean>();
+    }
+
+    ngOnInit() {
+
+        this.create = new FormGroup({
+            // TODO: replace this control with a patron search form
+            // when available in the Angular client
+            'patronBarcode': new FormControl('',
+                [Validators.required],
+                [this.pbv.validate]
+            ),
+            'emailNotify': new FormControl(true),
+            'startTime': new FormControl(),
+            'endTime': new FormControl(),
+            'resourceList': new FormControl(),
+        }, [startTimeIsBeforeEndTimeValidator]
+        );
+
+        this.addBresv$ = () => {
+            let selectedResourceId = this.targetResource ? [this.targetResource] : null;
+            if (!selectedResourceId &&
+                this.resourceListSelection !== null &&
+                'any' !== this.resourceListSelection.id) {
+                selectedResourceId = [this.resourceListSelection.id];
+            }
+            return this.net.request(
+                'open-ils.booking',
+                'open-ils.booking.reservations.create',
+                this.auth.token(),
+                this.patronBarcode.value,
+                this.selectedTimes,
+                this.pickupLibId,
+                this.targetResourceType.id,
+                selectedResourceId,
+                this.attributes.filter(Boolean),
+                this.emailNotify
+            ).pipe(tap(
+                (success) => {
+                    if (success.ilsevent) {
+                        console.warn(success);
+                        this.fail.open();
+                    } else {
+                        this.toast.success('Reservation successfully created');
+                        console.debug(success);
+                        this.close();
+                   }
+                }, (fail) => {
+                    console.warn(fail);
+                    this.fail.open();
+                }, () => this.onComplete.emit(true)
+            ));
+        };
+
+        this.handlePickupLibChange = ($event) => {
+            this.pickupLibId = $event.id();
+            this.org.settings('lib.timezone', this.pickupLibId).then((tz) => {
+                this.timezone = tz['lib.timezone'] || this.format.wsOrgTimezone;
+                this.pickupLibraryUsesDifferentTz = (tz['lib.timezone'] && (this.format.wsOrgTimezone !== tz['lib.timezone']));
+            });
+        };
+
+        this.disableOrgs = () => this.org.filterList( { canHaveVolumes : false }, true);
+
+        this.patron$ = this.patronBarcode.statusChanges.pipe(
+            startWith({first_given_name: '', second_given_name: '', family_name: ''}),
+            switchMap(() => {
+                if ('VALID' === this.patronBarcode.status) {
+                    return this.net.request(
+                        'open-ils.actor',
+                        'open-ils.actor.get_barcodes',
+                        this.auth.token(),
+                        this.auth.user().ws_ou(),
+                        'actor', this.patronBarcode.value).pipe(
+                            single(),
+                            switchMap((result) => {
+                                return this.pcrud.retrieve('au', result[0]['id']).pipe(
+                                    switchMap((au) => {
+                                        return of({
+                                            first_given_name: au.first_given_name(),
+                                            second_given_name: au.second_given_name(),
+                                            family_name: au.family_name()});
+                                    })
+                                );
+                            })
+                        );
+                } else {
+                    return of({
+                        first_given_name: '',
+                        second_given_name: '',
+                        family_name: ''
+                    });
+                }
+            })
+        );
+    }
+
+    setDefaultTimes(times: Moment[], granularity: number) {
+        this.create.patchValue({startTime: Moment.min(times),
+        endTime: Moment.max(times).clone().add(granularity, 'minutes')
+        });
+    }
+
+    openPatronReservations = (): void => {
+        this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.get_barcodes',
+            this.auth.token(),
+            this.auth.user().ws_ou(),
+            'actor', this.patronBarcode.value
+        ).subscribe((patron) => this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_patron', patron[0]['id']]));
+    }
+
+    addBresvAndOpenPatronReservations = (): void => {
+        this.addBresv$()
+        .subscribe(() => this.openPatronReservations());
+    }
+
+    get emailNotify() {
+        return this.create.get('emailNotify').value;
+    }
+
+    get patronBarcode() {
+        return this.create.get('patronBarcode');
+    }
+
+    get resourceListSelection() {
+      return this.create.get('resourceList').value;
+    }
+
+    get selectedTimes() {
+        return [this.create.get('startTime').value.toISOString(),
+            this.create.get('endTime').value.toISOString()];
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.html b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.html
new file mode 100644
index 0000000000..a658af65d4
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.html
@@ -0,0 +1,219 @@
+<eg-staff-banner bannerText="Create Reservation" i18n-bannerText>
+</eg-staff-banner>
+<eg-title i18n-prefix i18n-suffix prefix="Booking" suffix="Create Reservation"></eg-title>
+{{attributes | json}}
+{{selectedAttributes.value | json}}
+<form [formGroup]="criteria" class="row">
+  <div class="col-sm-6">
+    <div class="row">
+      <div class="col">
+        <div class="input-group">
+          <div class="input-group-prepend">
+            <label class="input-group-text" for="ideal-reservation-type" i18n>Reservation type</label>
+          </div>
+          <select class="form-control" id="ideal-reservation-type" formControlName="reservationType">
+            <option *ngFor="let type of reservationTypes" [ngValue]="type" i18n>{{type.name}}</option>
+          </select>
+        </div>
+      </div>
+      <div class="col">
+        <div class="input-group">
+          <div class="input-group-prepend">
+            <label class="input-group-text" for="ideal-reservation-date" i18n>Reservation date</label>
+          </div>
+          <eg-date-select *ngIf="!multiday" #dateLimiter domId="ideal-reservation-date" formControlName="idealDate"></eg-date-select>
+          <eg-daterange-select *ngIf="multiday" formControlName="idealDateRange"></eg-daterange-select>
+        </div>
+      </div>
+    </div>
+  </div>
+  <div class="card col-sm-6">
+    <h2 class="card-header" i18n>Reservation details</h2>
+    <ngb-tabset #details="ngbTabset">
+      <ngb-tab id="select-resource-type">
+        <ng-template ngbTabTitle>
+          <span class="material-icons">category</span>
+          <ng-container i18n>Choose resource by type</ng-container>
+        </ng-template>
+        <ng-template ngbTabContent>
+          <div ngbPanelContent class="row">
+            <div class="col">
+              <div class="input-group">
+                <div class="input-group-prepend">
+                  <label class="input-group-text" for="ideal-resource-type" i18n>Search by resource type</label>
+                </div>
+                <eg-combobox
+                  formControlName="resourceType"
+                  domId="ideal-resource-type"
+                  idlClass="brt"
+                  [asyncSupportsEmptyTermClick]="true">
+                </eg-combobox>
+              </div>
+              <div class="col">
+                <eg-org-family-select [hideAncestorSelector]="true" labelText="Owning library" i18n-labelText formControlName="owningLibrary">
+                </eg-org-family-select>
+              </div>
+            </div>
+          </div>
+        </ng-template>
+      </ngb-tab>
+
+      <ngb-tab id="select-resource">
+        <ng-template ngbTabTitle>
+          <span class="material-icons">assignment</span>
+          <ng-container i18n>Choose resource by barcode</ng-container>
+        </ng-template>
+        <ng-template ngbTabContent>
+          <div ngbPanelContent class="row">
+            <div class="col">
+              <div class="input-group">
+                <div class="input-group-prepend">
+                  <label class="input-group-text" for="ideal-resource-barcode" i18n>Search by resource barcode</label>
+                </div>
+                <input type="text" id="ideal-resource-barcode" class="form-control" formControlName="resourceBarcode">
+              </div>
+            </div>
+          </div>
+        </ng-template>
+      </ngb-tab>
+
+      <ngb-tab id="attributes" [disabled]="0 === attributes.length">
+        <ng-template ngbTabTitle>
+          <span class="material-icons">filter_list</span>
+          <ng-container i18n>Limit by attributes</ng-container>
+        </ng-template>
+        <ng-template ngbTabContent>
+          <ul class="list-group list-group-flush" formArrayName="selectedAttributes">
+            <li *ngFor="let attribute of attributes; let i = index" class="list-group-item">
+              <span class="input-group">
+                <span class="input-group-prepend">
+                  <label class="input-group-text" for="attribute-{{attribute.id()}}" i18n>{{attribute.name()}}</label>
+                </span>
+                <eg-combobox [formControlName]="i">
+                  <eg-combobox-entry *ngFor="let value of attribute.valid_values()"
+                    [entryId]="value.id()" [entryLabel]="value.valid_value()">
+                  </eg-combobox-entry>
+                </eg-combobox>
+              </span>
+            </li>
+          </ul>
+        </ng-template>
+      </ngb-tab>
+
+      <ngb-tab id="display-settings">
+        <ng-template ngbTabTitle>
+          <span class="material-icons">settings</span>
+          <ng-container i18n>Schedule settings</ng-container>
+        </ng-template>
+        <ng-template ngbTabContent>
+          <ul class="list-group list-group-flush">
+            <li class="list-group-item">
+              <span class="input-group">
+                <span class="input-group-prepend">
+                  <label class="input-group-text" for="start-time" i18n>Start time</label>
+                </span>
+                <ngb-timepicker formControlName="startOfDay" [minuteStep]="minuteStep()" [meridian]="true"></ngb-timepicker>
+              </span>
+            </li>
+            <li class="list-group-item">
+              <span class="input-group">
+                <span class="input-group-prepend">
+                  <label class="input-group-text" for="end-time" i18n>End time</label>
+                </span>
+                <ngb-timepicker formControlName="endOfDay" [minuteStep]="minuteStep()" [meridian]="true"></ngb-timepicker>
+              </span>
+            </li>
+            <li *ngIf="criteria.errors && criteria.errors.startOfDayNotBeforeEndOfDay" class="list-group-item">
+              <div role="alert" class="alert alert-danger">
+                <span class="material-icons" aria-hidden="true">error</span>
+                <span i18n>Start time must be before end time</span>
+              </div>
+            </li>
+            <li class="list-group-item">
+              <span class="input-group">
+                <span class="input-group-prepend">
+                  <label class="input-group-text" for="granularity" i18n>Granularity</label>
+                </span>
+                <eg-combobox (onChange)="changeGranularity($event)" [startId]="granularity ? granularity : 30">
+                  <eg-combobox-entry [entryId]="15" entryLabel="15 minutes"
+                    i18n-entryLabel></eg-combobox-entry>
+                  <eg-combobox-entry [entryId]="30" entryLabel="30 minutes"
+                    i18n-entryLabel></eg-combobox-entry>
+                  <eg-combobox-entry [entryId]="60" entryLabel="60 minutes"
+                    i18n-entryLabel></eg-combobox-entry>
+                </eg-combobox>
+              </span>
+            </li>
+          </ul>
+        </ng-template>
+      </ngb-tab>
+    </ngb-tabset>
+  </div>
+</form>
+
+<ng-container *ngIf="resources.length">
+  <hr>
+  <div class="row" *ngIf="idealDate && !multiday">
+    <button class="btn btn-info col-sm-2 offset-sm-3" (click)="addDays(-1)">
+        <span class="material-icons mat-icon-in-button">keyboard_arrow_left</span>
+        <span i18n>Previous day</span>
+    </button>
+    <h2 class="col-sm-2 text-center" i18n>{{idealDate | formatValue:'timestamp'}}</h2>
+    <button class="btn btn-info col-sm-2" (click)="addDays(1)">
+      <span i18n>Next day</span>
+      <span class="material-icons mat-icon-in-button">keyboard_arrow_right</span>
+    </button>
+  </div>
+  <eg-grid #scheduleGrid
+    [sortable]="false"
+    (onRowActivate)="openTheDialog([$event])"
+    [dataSource]="scheduleSource"
+    [rowFlairIsEnabled]="true"
+    [rowFlairCallback]="resourceAvailabilityIcon"
+    [disablePaging]="true"
+    persistKey="disabled">
+    <eg-grid-toolbar-action label="Create Reservation" i18n-label (onClick)="openTheDialog($event)"></eg-grid-toolbar-action>
+    <eg-grid-column path="time" [index]="true" name="Time" i18n-name [cellTemplate]="timeTemplate" ></eg-grid-column>
+    <eg-grid-column *ngFor="let resource of resources" path="{{resource.barcode()}}" [cellTemplate]="reservationsTemplate" [disableTooltip]="true"></eg-grid-column>
+  </eg-grid>
+</ng-container>
+<div class="text-sm-center" *ngIf="this.resourceType.value && !resources.length" i18n>
+  There are no bookable resource that match your criteria.
+  Would you like to create <a [routerLink]="['/staff', 'admin', 'booking', 'splash']">some new resources</a>?
+</div>
+
+<eg-create-reservation-dialog #createDialog
+  (onComplete)="fetchData()"
+  [targetResourceBarcode]="resourceBarcode"
+  [targetResource]="resourceId"
+  [targetResourceType]="resourceType.value"
+  [attributes]="flattenedSelectedAttributes"
+  [resources]="resources">
+</eg-create-reservation-dialog>
+
+<ng-template #reservationsTemplate let-row="row" let-col="col">
+  <ng-container *ngIf="row[col.name]">
+    <ul class="list-unstyled">
+      <li *ngFor="let reservation of row[col.name]">
+        <button class="btn btn-info" (click)="openReservationViewer(reservation['reservationId'])">
+          {{reservation['patronLabel']}}
+        </button>
+      </li>
+    </ul>
+  </ng-container>
+</ng-template>
+<ng-template #timeTemplate let-row="row" let-col="col">
+  <ng-container *ngIf="!multiday">
+    {{row['time'].format('LT')}}
+  </ng-container>
+  <ng-container *ngIf="multiday">
+    {{row['time'] | formatValue:'timestamp'}}
+  </ng-container>
+</ng-template>
+<eg-fm-record-editor #viewReservation
+  idlClass="bresv"
+  datetimeFields="start_time,end_time"
+  hiddenFields="xact_start,xact_finish,cancel_time,booking_interval">
+</eg-fm-record-editor>
+<eg-no-timezone-set-dialog #noTimezoneSetDialog>
+</eg-no-timezone-set-dialog>
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts
new file mode 100644
index 0000000000..23cddcb3ff
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts
@@ -0,0 +1,417 @@
+import { Component, OnInit, AfterViewInit, QueryList, ViewChildren, ViewChild, OnDestroy } from '@angular/core';
+import {FormGroup, FormControl, ValidationErrors, ValidatorFn, FormArray} from '@angular/forms';
+import {Router, ActivatedRoute} from '@angular/router';
+import {from, iif, Observable, of, throwError, timer, Subscription} from 'rxjs';
+import {catchError, debounceTime, takeLast, mapTo, single, switchMap, tap} from 'rxjs/operators';
+import {NgbCalendar, NgbTabset} from '@ng-bootstrap/ng-bootstrap';
+import {AuthService} from '@eg/core/auth.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {FormatService} from '@eg/core/format.service';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource, GridRowFlairEntry} from '@eg/share/grid/grid';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {CreateReservationDialogComponent} from './create-reservation-dialog.component';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {DateRange} from '@eg/share/daterange-select/daterange-select.component';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {ScheduleGridService, ScheduleRow} from './schedule-grid.service';
+import {NoTimezoneSetComponent} from './no-timezone-set.component';
+
+import * as Moment from 'moment-timezone';
+
+const startOfDayIsBeforeEndOfDayValidator: ValidatorFn = (fg: FormGroup): ValidationErrors | null => {
+    const start = fg.get('startOfDay').value;
+    const end = fg.get('endOfDay').value;
+    return start !== null && end !== null &&
+        (start.hour <= end.hour) &&
+        !((start.hour === end.hour) && (start.minute >= end.minute))
+        ? null
+        : { startOfDayNotBeforeEndOfDay: true };
+};
+
+ at Component({
+    templateUrl: './create-reservation.component.html',
+    styles: ['#ideal-resource-barcode {min-width: 300px;}']
+})
+export class CreateReservationComponent implements OnInit, AfterViewInit, OnDestroy {
+
+    criteria: FormGroup;
+
+    attributes: IdlObject[] = [];
+    multiday = false;
+    resourceAvailabilityIcon: (row: ScheduleRow) => GridRowFlairEntry;
+
+    patronBarcode: string;
+    patronId: number;
+    resourceBarcode: string;
+    resourceId: number;
+    transferable: boolean;
+    resourceOwner: number;
+    subscriptions: Subscription[] = [];
+
+    defaultGranularity = 30;
+    granularity: number = this.defaultGranularity;
+
+    scheduleSource: GridDataSource = new GridDataSource();
+
+    minuteStep: () => number;
+    reservationTypes: {id: string, name: string}[];
+
+    openTheDialog: (rows: IdlObject[]) => void;
+
+    resources: IdlObject[] = [];
+
+    setGranularity: () => void;
+    changeGranularity: ($event: ComboboxEntry) => void;
+
+    dateRange: DateRange;
+
+    @ViewChild('createDialog') createDialog: CreateReservationDialogComponent;
+    @ViewChild('details') details: NgbTabset;
+    @ViewChild('noTimezoneSetDialog') noTimezoneSetDialog: NoTimezoneSetComponent;
+    @ViewChild('viewReservation') viewReservation: FmRecordEditorComponent;
+    @ViewChildren('scheduleGrid') scheduleGrids: QueryList<GridComponent>;
+
+    constructor(
+        private auth: AuthService,
+        private calendar: NgbCalendar,
+        private format: FormatService,
+        private net: NetService,
+        private pcrud: PcrudService,
+        private route: ActivatedRoute,
+        private router: Router,
+        private scheduleService: ScheduleGridService,
+        private store: ServerStoreService,
+        private toast: ToastService,
+    ) {
+    }
+
+    ngOnInit() {
+        if (!(this.format.wsOrgTimezone)) {
+            this.noTimezoneSetDialog.open();
+        }
+
+        const initialRangeLength = 10;
+        const defaultRange = {
+            fromDate: this.calendar.getToday(),
+            toDate: this.calendar.getNext(
+                this.calendar.getToday(), 'd', initialRangeLength)
+        };
+
+        this.route.paramMap.pipe(
+            tap(params => {
+                this.patronId = +params.get('patron_id');
+                this.resourceBarcode = params.get('resource_barcode');
+            }),
+            switchMap(params => iif(() => params.has('resource_barcode'),
+                this.handleBarcodeFromUrl$(params.get('resource_barcode')),
+                of(params)
+            ))
+        ).subscribe({
+            error() {
+                console.warn('could not find a resource with this barcode');
+            }
+        });
+
+        this.reservationTypes = [
+            {id: 'single', name: 'Single day reservation'},
+            {id: 'multi', name: 'Multiple day reservation'},
+        ];
+
+        const waitToLoadResource = 800;
+        this.criteria = new FormGroup({
+            'resourceBarcode': new FormControl(this.resourceBarcode ? this.resourceBarcode : '',
+                [], (rb) =>
+                    timer(waitToLoadResource).pipe(switchMap(() =>
+                        this.pcrud.search('brsrc',
+                            {'barcode' : rb.value},
+                            {'limit': 1})),
+                        single(),
+                        mapTo(null),
+                        catchError(() => of({ resourceBarcode: 'No resource found with that barcode' }))
+                    )),
+            'resourceType': new FormControl(),
+            'startOfDay': new FormControl({hour: 9, minute: 0, second: 0}),
+            'endOfDay': new FormControl({hour: 17, minute: 0, second: 0}),
+            'idealDate': new FormControl(new Date()),
+            'idealDateRange': new FormControl(defaultRange),
+            'reservationType': new FormControl(),
+            'owningLibrary': new FormControl({primaryOrgId: this.auth.user().ws_ou(), includeDescendants: true}),
+            'selectedAttributes': new FormArray([]),
+        }, [ startOfDayIsBeforeEndOfDayValidator
+        ]);
+
+        const debouncing = 1500;
+        this.criteria.get('resourceBarcode').valueChanges
+        .pipe(debounceTime(debouncing))
+        .subscribe((barcode) => {
+            this.resources = [];
+            if ('INVALID' === this.criteria.get('resourceBarcode').status) {
+                this.toast.danger('No resource found with this barcode');
+            } else {
+                this.router.navigate(['/staff', 'booking', 'create_reservation', 'for_resource', barcode]);
+            }
+        });
+
+        this.subscriptions.push(
+            this.resourceType.valueChanges.pipe(
+                switchMap((value) => {
+                    this.resourceBarcode = null;
+                    this.resources = [];
+                    this.resourceId = null;
+                    this.attributes = [];
+                    // TODO: when we upgrade to Angular 8, this can
+                    // be simplified to this.selectedAttributes.clear();
+                    while (this.selectedAttributes.length) {
+                        this.selectedAttributes.removeAt(0);
+                     }
+                    if (value.id) {
+                        return this.pcrud.search('bra', {resource_type : value.id}, {
+                            order_by: 'name ASC',
+                            flesh: 1,
+                            flesh_fields: {'bra' : ['valid_values']}
+                        }).pipe(
+                            tap((attribute) => {
+                                this.attributes.push(attribute);
+                                this.selectedAttributes.push(new FormControl());
+                            })
+                        );
+                    } else {
+                        return of();
+                    }
+                })
+            ).subscribe(() => this.fetchData()));
+
+        this.criteria.get('reservationType').valueChanges.subscribe((val) => {
+            this.multiday = ('multi' === val.id);
+            this.store.setItem('eg.booking.create.multiday', this.multiday);
+        });
+
+        this.subscriptions.push(
+            this.owningLibraryFamily.valueChanges
+                .subscribe(() => this.resources = []));
+
+        this.subscriptions.push(
+            this.criteria.valueChanges
+                .subscribe(() => this.fetchData()));
+
+        this.store.getItem('eg.booking.create.multiday').then(multiday => {
+            if (multiday) { this.multiday = multiday; }
+            this.criteria.patchValue({reservationType:
+                this.multiday ? this.reservationTypes[1] : this.reservationTypes[0]
+            });
+        });
+
+        const minutesInADay = 1440;
+
+        this.setGranularity = () => {
+            if (this.multiday) { // multiday reservations always use day granularity
+                this.granularity = minutesInADay;
+            } else {
+                this.store.getItem('eg.booking.create.granularity').then(granularity => {
+                    if (granularity) {
+                        this.granularity = granularity;
+                    } else {
+                        this.granularity = this.defaultGranularity;
+                    }
+                });
+            }
+        };
+
+        this.criteria.get('idealDate').valueChanges
+            .pipe(switchMap((date) => this.scheduleService.hoursOfOperation(date)))
+            .subscribe((hours) => this.criteria.patchValue(hours, {emitEvent: false}),
+                () => {},
+                () => this.fetchData());
+
+        this.changeGranularity = ($event) => {
+            this.granularity = $event.id;
+            this.store.setItem('eg.booking.create.granularity', $event.id)
+            .then(() => this.fetchData());
+        };
+
+        const minutesInAnHour = 60;
+
+        this.minuteStep = () => {
+            return (this.granularity < minutesInAnHour) ? this.granularity : this.defaultGranularity;
+        };
+
+        this.resourceAvailabilityIcon = (row: ScheduleRow) => {
+            return this.scheduleService.resourceAvailabilityIcon(row,  this.resources.length);
+        };
+    }
+
+    ngAfterViewInit() {
+        this.fetchData();
+
+        this.openTheDialog = (rows: IdlObject[]) => {
+            if (rows && rows.length) {
+                this.createDialog.setDefaultTimes(rows.map((row) => row['time'].clone()), this.granularity);
+            }
+            this.subscriptions.push(
+                this.createDialog.open({size: 'lg'})
+                .subscribe(() => this.fetchData())
+            );
+        };
+    }
+
+    fetchData = (): void => {
+        this.setGranularity();
+        this.scheduleSource.data = [];
+        let resources$ = this.scheduleService.fetchRelevantResources(
+            this.resourceType.value ? this.resourceType.value.id : null,
+            this.owningLibraries,
+            this.flattenedSelectedAttributes
+        );
+        if (this.resourceId) {
+            resources$ = from(this.resources);
+        } else {
+            this.resources = [];
+        }
+
+        resources$.pipe(
+            tap((resource) => this.resources.push(resource)),
+            takeLast(1),
+            switchMap(() => {
+                let range = {startTime: Moment(), endTime: Moment()};
+
+                if (this.multiday) {
+                    range = this.scheduleService.momentizeDateRange(
+                        this.idealDateRange,
+                        this.format.wsOrgTimezone
+                    );
+                } else {
+                    range = this.scheduleService.momentizeDay(
+                        this.idealDate,
+                        this.userStartOfDay,
+                        this.userEndOfDay,
+                        this.format.wsOrgTimezone
+                    );
+                }
+                this.scheduleSource.data = this.scheduleService.createBasicSchedule(
+                    range, this.granularity);
+                return this.scheduleService.fetchReservations(range, this.resources.map(r => r.id()));
+            })
+        ).subscribe((reservation) => {
+            this.scheduleSource.data = this.scheduleService.addReservationToSchedule(
+                reservation,
+                this.scheduleSource.data,
+                this.granularity,
+                this.format.wsOrgTimezone
+            );
+        });
+    }
+    // TODO: make this into cross-field validation, and don't fetch data if true
+    invalidMultidaySettings(): boolean {
+        return (this.multiday && (!this.idealDateRange ||
+            (null == this.idealDateRange.fromDate) ||
+            (null == this.idealDateRange.toDate)));
+    }
+
+    handleBarcodeFromUrl$(barcode: string): Observable<any> {
+        return this.findResourceByBarcode$(barcode)
+        .pipe(
+            catchError(() => this.handleBrsrcError$(barcode)),
+            tap((resource) => {
+                if (resource) {
+                    this.resourceId = resource.id();
+                    this.criteria.patchValue({
+                        resourceType: {id: resource.type()}},
+                        {emitEvent: false});
+                    this.resources = [resource];
+                    this.details.select('select-resource');
+                    this.fetchData();
+                }
+            })
+        );
+    }
+
+    findResourceByBarcode$(barcode: string): Observable<IdlObject> {
+        return this.pcrud.search('brsrc',
+            {'barcode' : barcode}, {'limit': 1})
+            .pipe(single());
+    }
+
+    handleBrsrcError$(barcode: string): Observable<any> {
+        return this.tryToMakeThisBookable$(barcode)
+        .pipe(switchMap(() => this.findResourceByBarcode$(barcode)),
+            catchError(() => {
+                this.toast.danger('No resource found with this barcode');
+                this.resourceId = -1;
+                return throwError('could not find or create a resource');
+            }));
+    }
+
+    tryToMakeThisBookable$(barcode: string): Observable<any> {
+        return this.pcrud.search('acp',
+            {'barcode' : barcode}, {'limit': 1})
+        .pipe(single(),
+            switchMap((item) =>
+                this.net.request( 'open-ils.booking',
+                    'open-ils.booking.resources.create_from_copies',
+                    this.auth.token(), [item.id()])
+                ),
+            catchError(() => {
+                this.toast.danger('Cannot make this barcode bookable');
+                return throwError('Tried and failed to make that barcode bookable');
+            }),
+            tap((response) => {
+                this.toast.info('Made this barcode bookable');
+                this.resourceId = response['brsrc'][0][0];
+            }));
+    }
+
+    addDays = (days: number): void => {
+        const result = new Date(this.idealDate);
+        result.setDate(result.getDate() + days);
+        this.criteria.patchValue({idealDate: result});
+    }
+
+    openReservationViewer = (id: number): void => {
+        this.viewReservation.mode = 'view';
+        this.viewReservation.recId = id;
+        this.viewReservation.open({ size: 'lg' });
+    }
+
+    get resourceType() {
+        return this.criteria.get('resourceType');
+    }
+    get userStartOfDay() {
+        return this.criteria.get('startOfDay').value;
+    }
+    get userEndOfDay() {
+        return this.criteria.get('endOfDay').value;
+    }
+    get idealDate() {
+        return this.criteria.get('idealDate').value;
+    }
+    get idealDateRange() {
+        return this.criteria.get('idealDateRange').value;
+    }
+    get owningLibraryFamily() {
+        return this.criteria.get('owningLibrary');
+    }
+    get owningLibraries() {
+        if (this.criteria.get('owningLibrary').value.orgIds) {
+            return this.criteria.get('owningLibrary').value.orgIds;
+        } else {
+            return [this.criteria.get('owningLibrary').value.primaryOrgId];
+        }
+    }
+    get selectedAttributes() {
+        return <FormArray>this.criteria.get('selectedAttributes');
+    }
+    get flattenedSelectedAttributes(): number[] {
+        return this.selectedAttributes.value.filter(Boolean).map((entry) => entry.id);
+    }
+    ngOnDestroy(): void {
+        this.subscriptions.forEach((subscription) => {
+            subscription.unsubscribe();
+        });
+      }
+
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/manage-reservations.component.html b/Open-ILS/src/eg2/src/app/staff/booking/manage-reservations.component.html
new file mode 100644
index 0000000000..b625672a1b
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/manage-reservations.component.html
@@ -0,0 +1,72 @@
+<eg-staff-banner bannerText="Manage Reservations" i18n-bannerText>
+</eg-staff-banner>
+<eg-title i18n-prefix i18n-suffix prefix="Booking" suffix="Manage Reservations"></eg-title>
+
+<form [formGroup]="filters" class="row">
+  <div class="col-sm-3">
+    <eg-org-family-select [hideAncestorSelector]="true" labelText="Pickup library" i18n-labelText formControlName="pickupLibraries">
+    </eg-org-family-select>
+  </div>
+  <div class="col-sm-6 offset-sm-3">
+    <div class="card">
+      <h2 class="card-header" i18n>Filter reservations</h2>
+      <ngb-tabset #filterTabs [activeId]="startingTab" class="mt-1">
+        <ngb-tab id="patron">
+          <ng-template ngbTabTitle>
+            <span class="material-icons" *ngIf="patronId">filter_list</span> <span i18n>Filter by patron</span>
+          </ng-template>
+          <ng-template ngbTabContent>
+            <div class="m-2">
+              <div class="input-group m-2">
+                <div class="input-group-prepend">
+                  <label class="input-group-text" for="patron-barcode-value" i18n>Patron barcode</label>
+                </div>
+                <input type="text" id="patron-barcode-value" class="form-control" formControlName="patronBarcode">
+                <div class="input-group-button">
+                  <button *ngIf="patronBarcode.value" class="btn btn-warning" (click)="removeFilters()" i18n><span class="material-icons">delete</span> Remove filter</button>
+                </div>
+              </div>
+            </div>
+          </ng-template>
+        </ngb-tab>
+        <ngb-tab id="resource">
+          <ng-template ngbTabTitle>
+            <span class="material-icons" *ngIf="resourceBarcode.value">filter_list</span> <span i18n>Filter by resource</span>
+          </ng-template>
+          <ng-template ngbTabContent>
+            <div class="m-2">
+              <div class="input-group m-2">
+                <div class="input-group-prepend">
+                  <label class="input-group-text" for="resource-barcode-value" i18n>Resource barcode</label>
+                </div>
+                <input type="text" id="resource-barcode-value" class="form-control" formControlName="resourceBarcode">
+                <div class="input-group-button">
+                  <button *ngIf="resourceBarcode.value" class="btn btn-warning" (click)="removeFilters()" i18n><span class="material-icons">delete</span> Remove filter</button>
+                </div>
+              </div>
+            </div>
+          </ng-template>
+        </ngb-tab>
+        <ngb-tab id="type">
+          <ng-template ngbTabTitle>
+            <span class="material-icons" *ngIf="resourceType.value">filter_list</span> <span i18n>Filter by resource type</span>
+          </ng-template>
+          <ng-template ngbTabContent>
+            <div class="m-2">
+              <div class="input-group m-2">
+                <div class="input-group-prepend">
+                  <label class="input-group-text" for="resource-type-value" i18n>Resource type</label>
+                </div>
+                <eg-combobox domId="resource-type-value" formControlName="resourceType" idlClass="brt" [asyncSupportsEmptyTermClick]="true"></eg-combobox>
+                <div class="input-group-button">
+                  <button class="btn btn-warning" (click)="removeFilters()" i18n><span class="material-icons">delete</span> Remove filter</button>
+                </div>
+              </div>
+            </div>
+          </ng-template>
+        </ngb-tab>
+      </ngb-tabset>
+    </div>
+  </div>
+</form>
+<eg-reservations-grid #reservationsGrid [patron]="patronId" [resourceBarcode]="resourceBarcode.value" [resourceType]="resourceTypeForGrid" [pickupLibIds]="pickupLibrariesForGrid" persistSuffix="manage"></eg-reservations-grid>
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/manage-reservations.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/manage-reservations.component.ts
new file mode 100644
index 0000000000..239e1bf01b
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/manage-reservations.component.ts
@@ -0,0 +1,188 @@
+import {Component, OnInit, ViewChild, OnDestroy} from '@angular/core';
+import {FormGroup, FormControl} from '@angular/forms';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {Subscription, of, from} from 'rxjs';
+import {debounceTime, single, tap, switchMap} from 'rxjs/operators';
+import {NgbTabset} from '@ng-bootstrap/ng-bootstrap';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {ReservationsGridComponent} from './reservations-grid.component';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {NetService} from '@eg/core/net.service';
+import {PatronBarcodeValidator} from '@eg/share/validators/patron_barcode_validator.directive';
+import {BookingResourceBarcodeValidator} from './booking_resource_validator.directive';
+import {OrgFamily} from '@eg/share/org-family-select/org-family-select.component';
+
+ at Component({
+    selector: 'eg-manage-reservations',
+    templateUrl: './manage-reservations.component.html',
+})
+export class ManageReservationsComponent implements OnInit, OnDestroy {
+
+    patronId: number;
+    resourceId: number;
+    subscriptions: Subscription[] = [];
+    filters: FormGroup;
+    startingTab: 'patron' | 'resource' | 'type' = 'patron';
+    startingPickupOrgs: OrgFamily = {primaryOrgId: this.auth.user().ws_ou(), includeDescendants: true};
+
+    @ViewChild('filterTabs') filterTabs: NgbTabset;
+    @ViewChild('reservationsGrid') reservationsGrid: ReservationsGridComponent;
+
+    removeFilters: () => void;
+
+    constructor(
+        private route: ActivatedRoute,
+        private router: Router,
+        private auth: AuthService,
+        private net: NetService,
+        private pcrud: PcrudService,
+        private store: ServerStoreService,
+        private toast: ToastService,
+        private patronValidator: PatronBarcodeValidator,
+        private resourceValidator: BookingResourceBarcodeValidator
+    ) {
+        this.store.getItem('eg.booking.manage.selected_org_family').then((pickupLibs) => {
+            if (pickupLibs) {
+                this.startingPickupOrgs = pickupLibs;
+            }
+        });
+    }
+
+    ngOnInit() {
+        this.filters = new FormGroup({
+            'pickupLibraries': new FormControl(this.startingPickupOrgs),
+            'patronBarcode': new FormControl('', [], [this.patronValidator.validate]),
+            'resourceBarcode': new FormControl('', [], [this.resourceValidator.validate]),
+            'resourceType': new FormControl(null),
+        });
+
+        const debouncing = 300;
+
+        this.subscriptions.push(
+            this.pickupLibraries.valueChanges.pipe(
+            ).subscribe(() => this.reservationsGrid.reloadGrid()));
+
+        this.subscriptions.push(
+            this.patronBarcode.statusChanges.pipe(
+                debounceTime(debouncing),
+                switchMap((status) => {
+                    if ('VALID' === status) {
+                        return this.net.request(
+                            'open-ils.actor',
+                            'open-ils.actor.get_barcodes',
+                            this.auth.token(), this.auth.user().ws_ou(),
+                           'actor', this.patronBarcode.value).pipe(
+                               single(),
+                               tap((response) =>
+                                   this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_patron', response[0].id])
+                        ));
+                    } else {
+                        this.toast.danger('No patron found with this barcode');
+                        return of();
+                    }})
+            ).subscribe());
+
+        this.subscriptions.push(
+            this.resourceBarcode.statusChanges.pipe(
+                debounceTime(debouncing),
+                tap((status) => {
+                    if ('VALID' === status) {
+                        if (this.resourceBarcode.value) {
+                            this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_resource', this.resourceBarcode.value]);
+                        } else {
+                            this.removeFilters();
+                        }
+                    }
+                }
+            )).subscribe());
+
+        this.subscriptions.push(
+            this.resourceType.valueChanges.pipe(
+                tap((value) => {
+                    if (value) {
+                        this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_resource_type', value.id]);
+                    } else {
+                        this.removeFilters();
+                    }
+                }
+            )).subscribe());
+
+            this.subscriptions.push(
+                this.pickupLibraries.valueChanges.pipe(
+                    tap((value) =>  this.store.setItem('eg.booking.manage.selected_org_family', value))
+                ).subscribe());
+
+        this.removeFilters = () => {
+              this.router.navigate(['/staff', 'booking', 'manage_reservations']);
+        };
+
+
+        this.route.paramMap.pipe(
+            switchMap((params: ParamMap) => {
+                this.patronId = params.has('patron_id') ? +params.get('patron_id') : null;
+                this.filters.patchValue({resourceBarcode: params.get('resource_barcode')}, {emitEvent: false});
+                this.filters.patchValue({resourceType: {id: +params.get('resource_type_id')}}, {emitEvent: false});
+
+                if (this.patronId) {
+                    return this.pcrud.search('au', {
+                        'id': this.patronId,
+                    }, {
+                        limit: 1,
+                        flesh: 1,
+                        flesh_fields: {'au': ['card']}
+                    }).pipe(tap(
+                        (resp) => {
+                            this.filters.patchValue({patronBarcode: resp.card().barcode()}); },
+                        (err) => { console.debug(err); }
+                    ));
+                } else if (this.resourceBarcode.value) {
+                    this.startingTab = 'resource';
+                    return this.pcrud.search('brsrc',
+                    {'barcode' : this.resourceBarcode.value}, {'limit': 1}).pipe(
+                    tap((res) => {
+                        this.resourceId = res.id();
+                    }, (err) => {
+                        this.resourceId = -1;
+                        this.toast.danger('No resource found with this barcode');
+                    }));
+                } else if (this.resourceType.value) {
+                    this.startingTab = 'type';
+                    return of(null);
+                } else {
+                    return of(null);
+                }
+
+        })).subscribe();
+    }
+
+    get pickupLibraries() {
+        return this.filters.get('pickupLibraries');
+    }
+    get patronBarcode() {
+      return this.filters.get('patronBarcode');
+    }
+    get resourceBarcode() {
+        return this.filters.get('resourceBarcode');
+    }
+    get resourceType() {
+        return this.filters.get('resourceType');
+    }
+    get pickupLibrariesForGrid() {
+        return this.pickupLibraries.value ?
+            this.pickupLibraries.value.orgIds :
+            [this.auth.user().ws_ou()];
+    }
+    get resourceTypeForGrid() {
+        return this.resourceType.value ? this.resourceType.value.id : null;
+    }
+
+    ngOnDestroy(): void {
+        this.subscriptions.forEach((subscription) => {
+            subscription.unsubscribe();
+        });
+    }
+
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/no-timezone-set.component.html b/Open-ILS/src/eg2/src/app/staff/booking/no-timezone-set.component.html
new file mode 100644
index 0000000000..9d8e646e81
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/no-timezone-set.component.html
@@ -0,0 +1,17 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>Timezone not set for your library</h4>
+    <button type="button" class="close"
+      i18n-aria-label aria-label="Close"
+      (click)="dismiss('cross_click')">
+      <span aria-hidden="true">×</span>
+    </button>
+  </div>
+  <div class="modal-body" i18n><p>Please make sure that <i>lib.timezone</i> has a valid value in the Library Settings Editor.</p></div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-success"
+      (click)="openLSE()" i18n>Go to Library Settings Editor</button>
+    <button type="button" class="btn btn-warning"
+      (click)="dismiss('canceled')" i18n>Continue anyway</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/no-timezone-set.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/no-timezone-set.component.ts
new file mode 100644
index 0000000000..c613d1f7a4
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/no-timezone-set.component.ts
@@ -0,0 +1,16 @@
+import {Component} from '@angular/core';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+
+ at Component({
+  selector: 'eg-no-timezone-set-dialog',
+  templateUrl: './no-timezone-set.component.html'
+})
+
+/**
+ * Dialog that warns users that there is no valid lib.timezone setting
+ */
+export class NoTimezoneSetComponent extends DialogComponent {
+    openLSE(): void {
+        window.open('/eg/staff/admin/local/asset/org_unit_settings', '_blank');
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/pickup.component.html b/Open-ILS/src/eg2/src/app/staff/booking/pickup.component.html
new file mode 100644
index 0000000000..0ec465d8ec
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/pickup.component.html
@@ -0,0 +1,27 @@
+<eg-staff-banner bannerText="Booking Pickup" i18n-bannerText>
+</eg-staff-banner>
+<eg-title i18n-prefix i18n-suffix prefix="Booking" suffix="Pickup"></eg-title>
+
+<form [formGroup]="findPatron" class="row">
+  <div class="col-md-4">
+    <div class="input-group flex-nowrap">
+      <div class="input-group-prepend">
+        <label class="input-group-text" for="patron-barcode" i18n>Patron barcode</label>
+        <input type="text" id="patron-barcode" class="form-control" formControlName="patronBarcode">
+      </div>
+    </div>
+  </div>
+</form>
+<div *ngIf="patronId">
+  <h2 class="text-center" i18n>Ready for pickup</h2>
+  <div class="form-check">
+    <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-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>
+
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/pickup.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/pickup.component.ts
new file mode 100644
index 0000000000..cec32c2df9
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/pickup.component.ts
@@ -0,0 +1,110 @@
+import {Component, OnInit, ViewChild, OnDestroy} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {Subscription, of} from 'rxjs';
+import {single, filter, switchMap, debounceTime, tap} from 'rxjs/operators';
+import {PatronService} from '@eg/staff/share/patron.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {IdlObject} from '@eg/core/idl.service';
+import {ReservationsGridComponent} from './reservations-grid.component';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {FormControl, FormGroup, Validators} from '@angular/forms';
+import {PatronBarcodeValidator} from '@eg/share/validators/patron_barcode_validator.directive';
+
+
+ at Component({
+  templateUrl: './pickup.component.html'
+})
+
+export class PickupComponent implements OnInit, OnDestroy {
+    patronId: number;
+    findPatron: FormGroup;
+    subscriptions: Subscription[] = [];
+    onlyShowCaptured = true;
+
+    @ViewChild('readyGrid') readyGrid: ReservationsGridComponent;
+    @ViewChild('pickedUpGrid') pickedUpGrid: ReservationsGridComponent;
+
+    noSelectedRows: (rows: IdlObject[]) => boolean;
+    handleShowCapturedChange: () => void;
+    retrievePatron: () => void;
+
+    constructor(
+        private pcrud: PcrudService,
+        private patron: PatronService,
+        private pbv: PatronBarcodeValidator,
+        private route: ActivatedRoute,
+        private router: Router,
+        private store: ServerStoreService,
+        private toast: ToastService
+    ) {
+    }
+
+
+    ngOnInit() {
+        this.findPatron = new FormGroup({
+            'patronBarcode': new FormControl(null,
+                [Validators.required],
+                [this.pbv.validate])
+        });
+
+        this.route.paramMap.pipe(
+            filter((params: ParamMap) => params.has('patron_id')),
+            switchMap((params: ParamMap) => {
+                this.patronId = +params.get('patron_id');
+                return this.pcrud.search('au', {
+                    'id': this.patronId,
+                }, {
+                    limit: 1,
+                    flesh: 1,
+                    flesh_fields: {'au': ['card']}});
+            })
+        ).subscribe(
+            (response) => {
+                this.findPatron.patchValue({patronBarcode: response.card().barcode()}, {emitEvent: false});
+                this.readyGrid.reloadGrid();
+                this.pickedUpGrid.reloadGrid();
+            }
+        );
+
+        const debouncing = 1500;
+        this.subscriptions.push(
+            this.patronBarcode.valueChanges.pipe(
+                debounceTime(debouncing),
+                switchMap((val) => {
+                    if ('INVALID' === this.patronBarcode.status) {
+                        this.toast.danger('No patron found with this barcode');
+                        return of();
+                    } else {
+                        return this.patron.bcSearch(val).pipe(
+                            single(),
+                            tap((resp) => { this.router.navigate(['/staff', 'booking', 'pickup', 'by_patron', resp[0].id]); })
+                        );
+                    }
+                })
+            )
+            .subscribe());
+
+
+        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.reloadGrid();
+            this.store.setItem('eg.booking.pickup.ready.only_show_captured', this.onlyShowCaptured);
+        };
+
+
+    }
+    get patronBarcode() {
+        return this.findPatron.get('patronBarcode');
+    }
+
+    ngOnDestroy(): void {
+        this.subscriptions.forEach((subscription) => {
+            subscription.unsubscribe();
+        });
+    }
+
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/pull-list.component.html b/Open-ILS/src/eg2/src/app/staff/booking/pull-list.component.html
new file mode 100644
index 0000000000..d6715dd59e
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/pull-list.component.html
@@ -0,0 +1,47 @@
+<eg-staff-banner bannerText="Booking Pull List" i18n-bannerText>
+</eg-staff-banner>
+<eg-title i18n-prefix i18n-suffix prefix="Booking" suffix="Pull List"></eg-title>
+
+<form [formGroup]="pullListCriteria" class="row">
+  <div class="col-md-4">
+    <div class="input-group">
+      <div class="input-group-prepend">
+        <label for="ou" class="input-group-text" i18n>Library:</label>
+      </div>
+      <eg-org-select domId="ou" [applyDefault]="true"
+        (onChange)="fillGrid($event.id())"
+        [disableOrgs]="disableOrgs()" [hideOrgs]="disableOrgs()">
+      </eg-org-select>
+    </div>
+  </div>
+  <div class="col-md-4">
+    <div class="input-group">
+      <div class="input-group-prepend">
+        <label for="days-hence" class="input-group-text" i18n>Number of days to fetch:</label>
+      </div>
+      <input type="number" min="1" class="form-control" formControlName="daysHence">
+    </div>
+  </div>
+</form>
+<eg-grid [dataSource]="dataSource" [useLocalSort]="true" #pullList
+  [sortable]="true" persistKey="booking.pull_list">
+  <eg-grid-toolbar-action label="Cancel Selected" i18n-label (onClick)="cancelSelected($event)" [disableOnRows]="noSelectedRows"></eg-grid-toolbar-action>
+  <eg-grid-toolbar-action label="View Item Status" i18n-label (onClick)="viewItemStatus($event)" [disableOnRows]="notOneCatalogedItemSelected"></eg-grid-toolbar-action>
+  <eg-grid-toolbar-action label="View Reservations for This Resource" i18n-label (onClick)="viewByResource($event)" [disableOnRows]="notOneResourceSelected"></eg-grid-toolbar-action>
+  <eg-grid-toolbar-action label="Print Pull List" i18n-label (onClick)="pullList.print()"></eg-grid-toolbar-action>
+
+  <eg-grid-column name="id" [hidden]="true" [index]="true" i18n-label label="ID" path="current_resource.id"></eg-grid-column>
+  <eg-grid-column label="Shelving location" path="shelving_location" i18n-label></eg-grid-column>
+  <eg-grid-column label="Call number" path="call_number" i18n-label></eg-grid-column>
+  <eg-grid-column label="Call number sortkey" path="call_number_sortkey" i18n-label></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-column label="Patron first name" path="reservations.0.usr.first_given_name" i18n-label></eg-grid-column>
+  <eg-grid-column label="Patron last name" path="reservations.0.usr.family_name" i18n-label></eg-grid-column>
+</eg-grid>
+
+<eg-cancel-reservation-dialog #confirmCancelReservationDialog
+  (onSuccessfulCancel)="fillGrid()">
+</eg-cancel-reservation-dialog>
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/pull-list.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/pull-list.component.ts
new file mode 100644
index 0000000000..745c52d830
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/pull-list.component.ts
@@ -0,0 +1,127 @@
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {FormControl, FormGroup, Validators} from '@angular/forms';
+import {from, Observable, of} from 'rxjs';
+import {switchMap} from 'rxjs/operators';
+import {AuthService} from '@eg/core/auth.service';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {ReservationActionsService} from './reservation-actions.service';
+import {CancelReservationDialogComponent} from './cancel-reservation-dialog.component';
+
+// The data that comes from the API, along with some fleshing
+interface PullListRow {
+    call_number?: string;
+    call_number_sortkey?: string;
+    current_resource: IdlObject;
+    reservations: IdlObject[];
+    shelving_location?: string;
+    target_resource_type: IdlObject;
+}
+
+ at Component({
+    templateUrl: './pull-list.component.html'
+})
+
+export class PullListComponent implements OnInit {
+    @ViewChild('confirmCancelReservationDialog')
+        private cancelReservationDialog: CancelReservationDialogComponent;
+
+    public dataSource: GridDataSource;
+
+    public disableOrgs: () => number[];
+    public fillGrid: (orgId?: number) => void;
+    pullListCriteria: FormGroup;
+
+    constructor(
+        private auth: AuthService,
+        private net: NetService,
+        private org: OrgService,
+        private pcrud: PcrudService,
+        private actions: ReservationActionsService,
+    ) { }
+
+
+    ngOnInit() {
+        this.dataSource = new GridDataSource();
+
+        const defaultDaysHence = 5;
+
+        this.pullListCriteria = new FormGroup({
+            'daysHence': new FormControl(defaultDaysHence, [
+                Validators.required,
+                Validators.min(1)])
+        });
+
+        this.pullListCriteria.valueChanges.subscribe(() => this.fillGrid() );
+
+        this.disableOrgs = () => this.org.filterList( { canHaveVolumes : false }, true);
+
+        this.fillGrid = (orgId = this.auth.user().ws_ou()) => {
+            this.dataSource.data = [];
+            const numberOfSecondsInADay = 86400;
+            this.net.request(
+                'open-ils.booking', 'open-ils.booking.reservations.get_pull_list',
+                this.auth.token(), null,
+                (this.daysHence.value * numberOfSecondsInADay),
+                orgId
+            ).pipe(switchMap((resources) => from(resources)),
+                switchMap((resource: PullListRow) => this.fleshResource(resource))
+            )
+            .subscribe((resource) => this.dataSource.data.push(resource));
+        };
+    }
+
+    noSelectedRows = (rows: IdlObject[]) => (rows.length === 0);
+
+    notOneResourceSelected = (rows: IdlObject[]) => {
+        return this.actions.notOneUniqueSelected(
+            rows.map(row => { if (row['current_resource']) { return row['current_resource']['id']; }}));
+    }
+
+    notOneCatalogedItemSelected = (rows: IdlObject[]) => {
+        return this.actions.notOneUniqueSelected(
+            rows.filter(row => (row['current_resource'] && row['call_number']))
+            .map(row => row['current_resource'].id())
+        );
+    }
+
+    cancelSelected = (rows: IdlObject[]) => {
+        this.cancelReservationDialog.open(rows.map(row => row['reservations'][0].id()));
+    }
+
+    fleshResource = (resource: PullListRow): Observable<PullListRow> => {
+        if ('t' === resource['target_resource_type'].catalog_item()) {
+            return this.pcrud.search('acp', {
+                'barcode': resource['current_resource'].barcode()
+                }, {
+                    limit: 1,
+                    flesh: 1,
+                    flesh_fields: {'acp' : ['call_number', 'location' ]}
+            }).pipe(switchMap((acp) => {
+                resource['call_number'] = acp.call_number().label();
+                resource['call_number_sortkey'] = acp.call_number().label_sortkey();
+                resource['shelving_location'] = acp.location().name();
+                return of(resource);
+            }));
+        } else {
+            return of(resource);
+        }
+    }
+
+    viewByResource = (reservations: IdlObject[]) => {
+        this.actions.manageReservationsByResource(reservations[0]['current_resource'].barcode());
+    }
+
+    viewItemStatus = (reservations: IdlObject[]) => {
+        this.actions.viewItemStatus(reservations[0]['current_resource'].barcode());
+    }
+
+    get daysHence() {
+        return this.pullListCriteria.get('daysHence');
+    }
+
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/reservation-actions.service.ts b/Open-ILS/src/eg2/src/app/staff/booking/reservation-actions.service.ts
new file mode 100644
index 0000000000..5545d06225
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/reservation-actions.service.ts
@@ -0,0 +1,32 @@
+import {Injectable} from '@angular/core';
+import {Router} from '@angular/router';
+import {PcrudService} from '@eg/core/pcrud.service';
+
+// Some grid actions that are shared across booking grids
+
+ at Injectable({providedIn: 'root'})
+export class ReservationActionsService {
+
+    constructor(
+        private pcrud: PcrudService,
+        private router: Router,
+    ) {
+    }
+
+    manageReservationsByResource = (barcode: string) => {
+        this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_resource', barcode]);
+    }
+
+    viewItemStatus = (barcode: string) => {
+        this.pcrud.search('acp', { 'barcode': barcode }, { limit: 1 })
+        .subscribe((acp) => {
+            window.open('/eg/staff/cat/item/' + acp.id());
+        });
+    }
+
+    notOneUniqueSelected = (ids: number[]) => {
+        return (new Set(ids).size !== 1);
+    }
+
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/reservation-actions.spec.ts b/Open-ILS/src/eg2/src/app/staff/booking/reservation-actions.spec.ts
new file mode 100644
index 0000000000..10f8549a52
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/reservation-actions.spec.ts
@@ -0,0 +1,35 @@
+import { TestBed } from '@angular/core/testing';
+import { Router } from '@angular/router';
+import { PcrudService } from '@eg/core/pcrud.service';
+import { ReservationActionsService } from './reservation-actions.service';
+describe('ReservationActionsService', () => {
+    let service: ReservationActionsService;
+    const routerSpy = {
+        navigate: jasmine.createSpy('navigate')
+    };
+    beforeEach(() => {
+        const pcrudServiceStub = {};
+        TestBed.configureTestingModule({
+            providers: [
+                ReservationActionsService,
+                { provide: Router, useValue: routerSpy },
+                { provide: PcrudService, useValue: pcrudServiceStub }
+            ]
+        });
+        service = TestBed.get(ReservationActionsService);
+    });
+    it('can open the manage by barcode route', () => {
+        service.manageReservationsByResource('barcode123');
+        expect(routerSpy.navigate).toHaveBeenCalledWith(
+            ['/staff', 'booking', 'manage_reservations', 'by_resource', 'barcode123']);
+    });
+    it('recognizes 3 as one unique value', () => {
+        expect(service.notOneUniqueSelected([3])).toBe(false);
+    });
+    it('recognizes 1 1 as one unique value', () => {
+        expect(service.notOneUniqueSelected([1, 1])).toBe(false);
+    });
+    it('recognizes 2 3 as more than one unique value', () => {
+        expect(service.notOneUniqueSelected([2, 3])).toBe(true);
+    });
+});
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.html b/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.html
new file mode 100644
index 0000000000..ab8d923668
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.html
@@ -0,0 +1,69 @@
+<eg-grid #grid [dataSource]="gridSource"
+  (onRowActivate)="handleRowActivate($event)"
+  [sortable]="true"
+  [useLocalSort]="true"
+  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="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>
+  <eg-grid-toolbar-action label="View Reservations for This Patron" i18n-label (onClick)="viewByPatron($event)" [disableOnRows]="notOnePatronSelected"></eg-grid-toolbar-action>
+  <eg-grid-toolbar-action label="View Item Status" i18n-label (onClick)="viewItemStatus($event)" [disableOnRows]="notOneCatalogedItemSelected"></eg-grid-toolbar-action>
+  <eg-grid-toolbar-action label="View Reservations for This Resource" i18n-label (onClick)="viewByResource($event)" [disableOnRows]="notOneResourceSelected"></eg-grid-toolbar-action>
+  <eg-grid-toolbar-button *ngIf="!status" label="Create New Reservation" i18n-label (onClick)="redirectToCreate($event)"></eg-grid-toolbar-button>
+
+  <eg-grid-column name="id" [hidden]="true" [index]="true" i18n-label label="ID" path="id"></eg-grid-column>
+  <eg-grid-column label="Patron username" [hidden]="true" i18n-label path="usr.usrname"></eg-grid-column>
+  <eg-grid-column label="Patron barcode" i18n-label path="usr.card.barcode"></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>
+  <eg-grid-column label="Patron family name" i18n-label path="usr.family_name"></eg-grid-column>
+  <eg-grid-column name="start_time" label="Start Time" [datePlusTime]="true" i18n-label path="start_time" datatype="timestamp"></eg-grid-column>
+  <eg-grid-column name="end_time" label="End Time" [datePlusTime]="true" i18n-label path="end_time" datatype="timestamp"></eg-grid-column>
+  <eg-grid-column name="request_time" label="Request Time" [datePlusTime]="true" i18n-label path="request_time" datatype="timestamp"></eg-grid-column>
+  <eg-grid-column name="capture_time" label="Capture Time" [datePlusTime]="true" i18n-label path="capture_time" datatype="timestamp"></eg-grid-column>
+  <eg-grid-column name="pickup_time" label="Pickup Time" [datePlusTime]="true" i18n-label path="pickup_time" datatype="timestamp"></eg-grid-column>
+  <eg-grid-column label="Email notify" i18n-label [hidden]="true" path="email_notify" datatype="bool"></eg-grid-column>
+  <eg-grid-column i18n-label [hidden]="true" path="unrecovered" datatype="bool"></eg-grid-column>
+  <eg-grid-column label="Billing total" i18n-label path="billing_total" datatype="money"></eg-grid-column>
+  <eg-grid-column label="Payment total" i18n-label path="payment_total" datatype="money"></eg-grid-column>
+  <eg-grid-column label="Booking interval" i18n-label [hidden]="true" path="booking_interval" [hidden]="true"></eg-grid-column>
+  <eg-grid-column label="Fine interval" i18n-label [hidden]="true" path="fine_interval" [hidden]="true"></eg-grid-column>
+  <eg-grid-column label="Fine amount" i18n-label [hidden]="true" path="fine_amount" datatype="money"></eg-grid-column>
+  <eg-grid-column label="Maximum fine" i18n-label [hidden]="true" path="max_fine" datatype="money"></eg-grid-column>
+  <eg-grid-column i18n-label label="Resource Barcode" path="current_resource.barcode"></eg-grid-column>
+  <eg-grid-column i18n-label label="Note" path="note"></eg-grid-column>
+  <eg-grid-column i18n-label label="Resource Type" path="target_resource_type.name"></eg-grid-column>
+  <eg-grid-column label="Reservation length" i18n-label  path="length"></eg-grid-column>
+  <eg-grid-column label="Request library" i18n-label  path="request_lib.name"></eg-grid-column>
+  <eg-grid-column label="Pickup library" i18n-label path="pickup_lib.name"></eg-grid-column>
+  <eg-grid-column label="Pickup library timezone" i18n-label path="timezone"></eg-grid-column>
+
+</eg-grid>
+
+<eg-fm-record-editor #editDialog
+  idlClass="bresv"
+  datetimeFields="start_time,end_time"
+  [fieldOptions]="{end_time:{customTemplate:{template:endTimeTemplate}}}"
+  hiddenFields="xact_start,xact_finish,cancel_time,booking_interval"
+  [readonlyFields]="listReadOnlyFields()">
+</eg-fm-record-editor>
+<eg-cancel-reservation-dialog #confirmCancelReservationDialog
+  (onSuccessfulCancel)="grid.reload()">
+</eg-cancel-reservation-dialog>
+<eg-no-timezone-set-dialog #noTimezoneSetDialog>
+</eg-no-timezone-set-dialog>
+
+<ng-template #endTimeTemplate let-field="field" let-record="record">
+  <eg-datetime-select
+    domId="endTime"
+    ngModel
+    [showTZ]="editDialog.timezone"
+    [timezone]="editDialog.timezone"
+    [egNotBeforeMoment]="momentizeIsoString(record['start_time'](), editDialog.timezone)"
+    [readOnly]="field.readOnly"
+    (onChangeAsIso)="record[field.name]($event)"
+    initialIso="{{record[field.name]()}}">
+  </eg-datetime-select>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts
new file mode 100644
index 0000000000..b14a8111e2
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts
@@ -0,0 +1,302 @@
+import {Component, EventEmitter, Input, Output, OnInit, ViewChild} from '@angular/core';
+import {Router} from '@angular/router';
+import {Observable, from, of} from 'rxjs';
+import {tap, switchMap, mergeMap} from 'rxjs/operators';
+import {AuthService} from '@eg/core/auth.service';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {FormatService} from '@eg/core/format.service';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {Pager} from '@eg/share/util/pager';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {NoTimezoneSetComponent} from './no-timezone-set.component';
+import {ReservationActionsService} from './reservation-actions.service';
+import {CancelReservationDialogComponent} from './cancel-reservation-dialog.component';
+
+import * as Moment from 'moment-timezone';
+
+ at Component({
+    selector: 'eg-reservations-grid',
+    templateUrl: './reservations-grid.component.html',
+})
+export class ReservationsGridComponent implements OnInit {
+
+    @Input() patron: number;
+    @Input() resourceBarcode: string;
+    @Input() resourceType: number;
+    @Input() pickupLibIds: number[];
+    @Input() status: 'pickupReady' | 'pickedUp' | 'returnReady' | 'returnedToday';
+    @Input() persistSuffix: string;
+    @Input() onlyCaptured = false;
+
+    @Output() onPickup = new EventEmitter<IdlObject>();
+
+    gridSource: GridDataSource;
+    patronBarcode: string;
+    numRowsSelected: number;
+
+    @ViewChild('grid') grid: GridComponent;
+    @ViewChild('editDialog') editDialog: FmRecordEditorComponent;
+    @ViewChild('confirmCancelReservationDialog')
+        private cancelReservationDialog: CancelReservationDialogComponent;
+    @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;
+    viewByPatron: (rows: IdlObject[]) => void;
+    viewByResource: (rows: IdlObject[]) => void;
+    viewItemStatus: (rows: IdlObject[]) => void;
+    viewPatronRecord: (rows: IdlObject[]) => void;
+    listReadOnlyFields: () => string;
+
+    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;
+    editNotAppropriate: (rows: IdlObject[]) => boolean;
+    returnNotAppropriate: (rows: IdlObject[]) => boolean;
+
+    constructor(
+        private auth: AuthService,
+        private format: FormatService,
+        private pcrud: PcrudService,
+        private router: Router,
+        private toast: ToastService,
+        private net: NetService,
+        private org: OrgService,
+        private actions: ReservationActionsService,
+    ) {
+
+    }
+
+    ngOnInit() {
+        if (!(this.format.wsOrgTimezone)) {
+            this.noTimezoneSetDialog.open();
+        }
+
+        this.gridSource = new GridDataSource();
+
+        this.gridSource.getRows = (pager: Pager, sort: any[]): Observable<IdlObject> => {
+            const orderBy: any = {};
+            const where = {
+                'usr' : (this.patron ? this.patron : {'>' : 0}),
+                'target_resource_type' : (this.resourceType ? this.resourceType : {'>' : 0}),
+                'cancel_time' : null,
+                'xact_finish' : null,
+            };
+            if (this.resourceBarcode) {
+                where['current_resource'] = {'in':
+                    {'from': 'brsrc', 'select': {'brsrc': ['id']}, 'where': {'barcode': this.resourceBarcode}}};
+            }
+            if (this.pickupLibIds) {
+                where['pickup_lib'] = this.pickupLibIds;
+            }
+            if (this.onlyCaptured) {
+                where['capture_time'] = {'!=': null};
+            }
+
+            if (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) {
+                    where['return_time'] = {'>': Moment().startOf('day').toISOString()};
+                }
+            } else {
+                where['return_time'] = null;
+            }
+            if (sort.length) {
+                orderBy.bresv = sort[0].name + ' ' + sort[0].dir;
+            }
+            return this.pcrud.search('bresv', where,  {
+                order_by: orderBy,
+                limit: pager.limit,
+                offset: pager.offset,
+                flesh: 2,
+                flesh_fields: {'bresv' : [
+                    'usr', 'capture_staff', 'target_resource', 'target_resource_type', 'current_resource', 'request_lib', 'pickup_lib'
+                ], 'au': ['card'] }
+            }).pipe(mergeMap((row) => this.enrichRow$(row)));
+        };
+
+        this.editDialog.mode = 'update';
+        this.editSelected = (idlThings: IdlObject[]) => {
+            const editOneThing = (thing: IdlObject) => {
+                if (!thing) { return; }
+                this.showEditDialog(thing).subscribe(
+                    () => editOneThing(idlThings.shift()));
+            };
+           editOneThing(idlThings.shift()); };
+
+        this.cancelSelected = (reservations: IdlObject[]) => {
+            this.cancelReservationDialog.open(reservations.map(reservation => reservation.id()));
+        };
+
+        this.viewByResource = (reservations: IdlObject[]) => {
+            this.actions.manageReservationsByResource(reservations[0].current_resource().barcode());
+        };
+
+        this.viewByPatron = (reservations: IdlObject[]) => {
+            const patronIds = reservations.map(reservation => reservation.usr().id());
+            this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_patron', patronIds[0]]);
+        };
+
+        this.viewItemStatus = (reservations: IdlObject[]) => {
+            this.actions.viewItemStatus(reservations[0].current_resource().barcode());
+        };
+
+        this.viewPatronRecord = (reservations: IdlObject[]) => {
+            const patronIds = reservations.map(reservation => reservation.usr().id());
+            window.open('/eg/staff/circ/patron/' + patronIds[0] + '/checkout');
+        };
+
+        this.noSelectedRows = (rows: IdlObject[]) => (rows.length === 0);
+        this.notOnePatronSelected = (rows: IdlObject[]) => this.actions.notOneUniqueSelected(rows.map(row => row.usr().id()));
+        this.notOneResourceSelected = (rows: IdlObject[]) => {
+            return this.actions.notOneUniqueSelected(
+                rows.map(row => { if (row.current_resource()) { return row.current_resource().id(); }}));
+        };
+        this.notOneCatalogedItemSelected = (rows: IdlObject[]) => {
+            return this.actions.notOneUniqueSelected(
+                rows.filter(row => (row.current_resource() && 't' === row.target_resource_type().catalog_item()))
+                .map(row => row.current_resource().id())
+            );
+        };
+        this.cancelNotAppropriate = (rows: IdlObject[]) =>
+            (this.noSelectedRows(rows) || ['pickedUp', 'returnReady', 'returnedToday'].includes(this.status));
+        this.pickupNotAppropriate = (rows: IdlObject[]) => (this.noSelectedRows(rows) || ('pickupReady' !== this.status));
+        this.editNotAppropriate = (rows: IdlObject[]) => (this.noSelectedRows(rows) || ('returnedToday' === 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; }
+                });
+            }
+            return false;
+        };
+
+        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; }
+                this.returnResource(thing).subscribe(
+                    () => returnOne(reservations.shift()));
+            };
+            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(
+                   () => {
+                       this.onPickup.emit(reservation);
+                       this.grid.reload(); },
+               ));
+        };
+
+        this.returnResource = (reservation: IdlObject) => {
+            return this.net.request(
+               'open-ils.circ',
+               'open-ils.circ.reservation.return',
+               this.auth.token(),
+               {'patron_barcode': this.patronBarcode, 'reservation': reservation})
+               .pipe(tap(
+                   () => { this.grid.reload(); },
+               ));
+        };
+
+        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 && ('pickupReady' !== this.status)) { list = list + ',start_time'; }
+            if (this.status && ('returnedToday' === this.status)) { list = list + ',end_time'; }
+            return list;
+        };
+
+        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);
+            }
+        };
+
+        this.redirectToCreate = () => {
+            this.router.navigate(['/staff', 'booking', 'create_reservation']);
+        };
+    }
+
+    enrichRow$ = (row: IdlObject): Observable<IdlObject> => {
+        return from(this.org.settings('lib.timezone', row.pickup_lib().id())).pipe(
+            switchMap((tz) => {
+                row['length'] = Moment(row['end_time']()).from(Moment(row['start_time']()), true);
+                row['timezone'] = tz['lib.timezone'];
+                return of(row);
+            })
+        );
+    }
+
+    showEditDialog(idlThing: IdlObject) {
+        this.editDialog.recId = idlThing.id();
+        this.editDialog.timezone = idlThing['timezone'];
+        return this.editDialog.open({size: 'lg'}).pipe(tap(
+            () => {
+                this.toast.success('Reservation successfully updated'); // TODO: needs i18n, pluralization
+                this.grid.reload();
+            }
+        ));
+    }
+
+    filterByResourceBarcode(barcode: string) {
+        this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_resource', barcode]);
+    }
+
+    momentizeIsoString(isoString: string, timezone: string): Moment {
+        return this.format.momentizeIsoString(isoString, timezone);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/return.component.html b/Open-ILS/src/eg2/src/app/staff/booking/return.component.html
new file mode 100644
index 0000000000..262910fcb3
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/return.component.html
@@ -0,0 +1,46 @@
+<eg-staff-banner bannerText="Booking Return" i18n-bannerText>
+</eg-staff-banner>
+<eg-title i18n-prefix i18n-suffix prefix="Booking" suffix="Return"></eg-title>
+
+<form [formGroup]="findPatron">
+  <ngb-tabset (tabChange)="handleTabChange($event)" activeId="patron" #tabs>
+    <ngb-tab title="By patron" i18n-title id="patron">
+      <ng-template ngbTabContent>
+        <div class="row">
+          <div class="col-md-4">
+            <div class="input-group flex-nowrap">
+              <div class="input-group-prepend">
+                <label class="input-group-text" for="patron-barcode" i18n>Patron barcode</label>
+                <input type="text" id="patron-barcode" class="form-control" i18n-placeholder placeholder="Patron barcode" formControlName="patronBarcode">
+              </div>
+            </div>
+          </div>
+        </div>
+        <div *ngIf="patronId">
+          <h2 class="text-center" i18n>Ready for return</h2>
+          <eg-reservations-grid #readyGrid [patron]="patronId" status="returnReady" (onReturn)="refreshGrids()" persistSuffix="return.patron.picked_up"></eg-reservations-grid>
+
+          <h2 class="text-center" i18n>Returned today</h2>
+          <eg-reservations-grid #returnedGrid [patron]="patronId" status="returnedToday" persistSuffix="return.patron.returned"></eg-reservations-grid>
+        </div>
+      </ng-template>
+    </ngb-tab>
+    <ngb-tab title="By resource" i18n-title id="resource">
+      <ng-template ngbTabContent>
+        <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" i18n-placeholder placeholder="Resource barcode" formControlName="resourceBarcode">
+          </div>
+        </div>
+        <div *ngIf="patronId">
+          <h2 class="text-center" i18n>Ready for return</h2>
+          <eg-reservations-grid #readyGrid [patron]="patronId" status="returnReady" (onReturn)="this.returnedGrid.reloadGrid()" persistSuffix="return.resource.picked_up"></eg-reservations-grid>
+
+          <h2 class="text-center" i18n>Returned today</h2>
+          <eg-reservations-grid #returnedGrid [patron]="patronId" status="returnedToday" persistSuffix="return.resource.returned"></eg-reservations-grid>
+        </div>
+      </ng-template>
+    </ngb-tab>
+  </ngb-tabset>
+</form>
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/return.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/return.component.ts
new file mode 100644
index 0000000000..74fb95aa05
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/return.component.ts
@@ -0,0 +1,145 @@
+import {Component, OnInit, OnDestroy, QueryList, ViewChildren, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {FormGroup, FormControl, Validators} from '@angular/forms';
+import {NgbTabChangeEvent, NgbTabset} from '@ng-bootstrap/ng-bootstrap';
+import {Observable, from, of, Subscription} from 'rxjs';
+import { single, switchMap, tap, debounceTime } from 'rxjs/operators';
+import {PatronService} from '@eg/staff/share/patron.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {IdlObject} from '@eg/core/idl.service';
+import {ReservationsGridComponent} from './reservations-grid.component';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {PatronBarcodeValidator} from '@eg/share/validators/patron_barcode_validator.directive';
+
+
+ at Component({
+  templateUrl: './return.component.html'
+})
+
+export class ReturnComponent implements OnInit, OnDestroy {
+    patronId: number;
+    findPatron: FormGroup;
+    subscriptions: Subscription[] = [];
+
+    noSelectedRows: (rows: IdlObject[]) => boolean;
+    handleTabChange: ($event: NgbTabChangeEvent) => void;
+    @ViewChild('tabs') tabs: NgbTabset;
+    @ViewChildren(ReservationsGridComponent) grids: QueryList<ReservationsGridComponent>;
+
+    constructor(
+        private pcrud: PcrudService,
+        private patron: PatronService,
+        private pbv: PatronBarcodeValidator,
+        private route: ActivatedRoute,
+        private router: Router,
+        private store: ServerStoreService,
+        private toast: ToastService
+    ) {
+    }
+
+
+    ngOnInit() {
+        this.route.paramMap.pipe(switchMap((params: ParamMap) => {
+            return this.handleParams$(params);
+        })).subscribe();
+
+        this.findPatron = new FormGroup({
+            'patronBarcode': new FormControl(null,
+                [Validators.required],
+                [this.pbv.validate]),
+            'resourceBarcode': new FormControl(null,
+                [Validators.required])
+        });
+
+        const debouncing = 1500;
+        this.subscriptions.push(
+            this.patronBarcode.valueChanges.pipe(
+                debounceTime(debouncing),
+                switchMap((val) => {
+                    if ('INVALID' === this.patronBarcode.status) {
+                        this.toast.danger('No patron found with this barcode');
+                        return of();
+                    } else {
+                        return this.patron.bcSearch(val).pipe(
+                            single(),
+                            tap((resp) => { this.router.navigate(['/staff', 'booking', 'return', 'by_patron', resp[0].id]); })
+                        );
+                    }
+                })
+            )
+            .subscribe());
+
+        this.subscriptions.push(
+            this.resourceBarcode.valueChanges.pipe(
+                debounceTime(debouncing),
+                switchMap((val) => {
+                    if ('INVALID' !== this.resourceBarcode.status) {
+                        return this.pcrud.search('brsrc', {'barcode': val}, {
+                            order_by: {'curr_rsrcs': 'pickup_time DESC'},
+                            limit: 1,
+                            flesh: 1,
+                            flesh_fields: {'brsrc': ['curr_rsrcs']},
+                            select: {'curr_rsrcs': {'return_time': null, 'pickup_time': {'!=': null}}}
+                        }).pipe(tap((resp) => {
+                            if (resp.curr_rsrcs()[0].usr()) {
+                                this.patronId = resp.curr_rsrcs()[0].usr();
+                                this.refreshGrids();
+                            }
+                        }));
+                    } else {
+                        return of();
+                    }
+                })
+            ).subscribe()
+        );
+        this.noSelectedRows = (rows: IdlObject[]) => (rows.length === 0);
+
+        this.handleTabChange = ($event) => {
+            this.store.setItem('eg.booking.return.tab', $event.nextId)
+            .then(() => {
+                this.router.navigate(['/staff', 'booking', 'return']);
+                this.findPatron.patchValue({resourceBarcode: ''});
+                this.patronId = null;
+            });
+        };
+    }
+
+    handleParams$ = (params: ParamMap): Observable<any> => {
+      this.patronId = +params.get('patron_id');
+      if (this.patronId) {
+          return this.pcrud.search('au', {
+              'id': this.patronId,
+          }, {
+              limit: 1,
+              flesh: 1,
+              flesh_fields: {'au': ['card']}
+          }).pipe(tap(
+              (resp) => {
+                  this.findPatron.patchValue({patronBarcode: resp.card().barcode()});
+                  this.refreshGrids();
+              }, (err) => { console.debug(err); }
+          ));
+      } else {
+          return from(this.store.getItem('eg.booking.return.tab'))
+              .pipe(tap(tab => {
+                  if (tab) { this.tabs.select(tab); }
+          }));
+      }
+    }
+    refreshGrids = (): void => {
+        this.grids.forEach (grid => grid.reloadGrid());
+    }
+    get patronBarcode() {
+      return this.findPatron.get('patronBarcode');
+    }
+    get resourceBarcode() {
+      return this.findPatron.get('resourceBarcode');
+    }
+
+    ngOnDestroy(): void {
+      this.subscriptions.forEach((subscription) => {
+          subscription.unsubscribe();
+      });
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/booking/routing.module.ts
new file mode 100644
index 0000000000..bc12e96a45
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/routing.module.ts
@@ -0,0 +1,44 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {CreateReservationComponent} from './create-reservation.component';
+import {ManageReservationsComponent} from './manage-reservations.component';
+import {PickupComponent} from './pickup.component';
+import {PullListComponent} from './pull-list.component';
+import {ReturnComponent} from './return.component';
+
+const routes: Routes = [{
+  path: 'create_reservation',
+    children: [
+      {path: '', component: CreateReservationComponent},
+      {path: 'for_patron/:patron_id', component: CreateReservationComponent},
+      {path: 'for_resource/:resource_barcode', component: CreateReservationComponent},
+  ]}, {
+  path: 'manage_reservations',
+    children: [
+      {path: '', component: ManageReservationsComponent},
+      {path: 'by_patron/:patron_id', component: ManageReservationsComponent},
+      {path: 'by_resource/:resource_barcode', component: ManageReservationsComponent},
+      {path: 'by_resource_type/:resource_type_id', component: ManageReservationsComponent},
+  ]}, {
+  path: 'pickup',
+    children: [
+      {path: '', component: PickupComponent},
+      {path: 'by_patron/:patron_id', component: PickupComponent},
+  ]}, {
+  path: 'pull_list',
+  component: PullListComponent
+  }, {
+  path: 'return',
+    children: [
+      {path: '', component: ReturnComponent},
+      {path: 'by_patron/:patron_id', component: ReturnComponent},
+  ]},
+  ];
+
+ at NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule],
+  providers: []
+})
+
+export class BookingRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/schedule-grid.service.ts b/Open-ILS/src/eg2/src/app/staff/booking/schedule-grid.service.ts
new file mode 100644
index 0000000000..7c6823f6e1
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/schedule-grid.service.ts
@@ -0,0 +1,173 @@
+import {Injectable} from '@angular/core';
+import {Observable, of} from 'rxjs';
+import {switchMap} from 'rxjs/operators';
+import {NgbTimeStruct} from '@ng-bootstrap/ng-bootstrap';
+import {AuthService} from '@eg/core/auth.service';
+import {IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {GridRowFlairEntry} from '@eg/share/grid/grid';
+import {DateRange} from '@eg/share/daterange-select/daterange-select.component';
+
+import * as Moment from 'moment-timezone';
+
+export interface ReservationPatron {
+  patronId: number;
+  patronLabel: string;
+  reservationId: number;
+}
+
+export interface ScheduleRow {
+    time: Moment;
+    [key: string]: ReservationPatron[];
+}
+
+// Various methods that fetch data for and process the schedule of reservations
+
+ at Injectable({providedIn: 'root'})
+export class ScheduleGridService {
+
+    constructor(
+        private auth: AuthService,
+        private pcrud: PcrudService,
+    ) {
+    }
+    hoursOfOperation = (date: Date): Observable<{startOfDay: NgbTimeStruct, endOfDay: NgbTimeStruct}> => {
+        const defaultStartHour = 9;
+        const defaultEndHour = 17;
+        return this.pcrud.retrieve('aouhoo', this.auth.user().ws_ou())
+            .pipe(switchMap((hours) => {
+                const startArray = hours[this.evergreenStyleDow(date) + '_open']().split(':');
+                const endArray = hours[this.evergreenStyleDow(date) + '_close']().split(':');
+                return of({
+                    startOfDay: {
+                        hour: ('00' === startArray[0]) ? defaultStartHour : +startArray[0],
+                        minute: +startArray[1],
+                        second: 0},
+                    endOfDay: {
+                        hour: ('00' === endArray[0]) ? defaultEndHour : +endArray[0],
+                        minute: +endArray[1],
+                        second: 0}
+                });
+            }));
+    }
+
+    resourceAvailabilityIcon = (row: ScheduleRow, numResources: number): GridRowFlairEntry => {
+        let icon = {icon: 'event_busy', title: 'All resources are reserved at this time'};
+        let busyColumns = 0;
+        for (const key in row) {
+            if (row[key] instanceof Array && row[key].length) {
+                busyColumns += 1;
+            }
+        }
+        if (busyColumns < numResources) {
+            icon = {icon: 'event_available', title: 'Resources are available at this time'};
+        }
+        return icon;
+    }
+
+    fetchRelevantResources = (resourceTypeId: number, owningLibraries: number[], selectedAttributes: number[]): Observable<IdlObject> => {
+        const where = {
+            type: resourceTypeId,
+            owner: owningLibraries,
+        };
+
+        if (selectedAttributes.length) {
+            where['id'] = {'in':
+                {'from': 'bram', 'select': {'bram': ['resource']},
+                'where': {'value':  selectedAttributes}}};
+        }
+        return this.pcrud.search('brsrc', where, {
+            order_by: 'barcode ASC',
+            flesh: 1,
+            flesh_fields: {'brsrc': ['attr_maps']},
+        });
+    }
+
+    momentizeDateRange = (range: DateRange, timezone: string): {startTime: Moment, endTime: Moment} => {
+        return {
+            startTime: Moment.tz([
+                range.fromDate.year,
+                range.fromDate.month - 1,
+                range.fromDate.day],
+                timezone),
+            endTime: Moment.tz([
+                range.toDate.year,
+                range.toDate.month - 1,
+                range.toDate.day + 1],
+                timezone)
+        };
+    }
+    momentizeDay = (date: Date, start: NgbTimeStruct, end: NgbTimeStruct, timezone: string): {startTime: Moment, endTime: Moment} => {
+        return {
+            startTime: Moment.tz([
+                date.getFullYear(),
+                date.getMonth(),
+                date.getDate(),
+                start.hour,
+                start.minute],
+                timezone),
+            endTime: Moment.tz([
+                date.getFullYear(),
+                date.getMonth(),
+                date.getDate(),
+                end.hour,
+                end.minute],
+                timezone)
+        };
+    }
+
+    createBasicSchedule = (range: {startTime: Moment, endTime: Moment}, granularity: number): ScheduleRow[] => {
+        const currentTime = range.startTime.clone();
+        const schedule = [];
+        while (currentTime < range.endTime) {
+            schedule.push({'time': currentTime.clone()});
+            currentTime.add(granularity, 'minutes');
+        }
+        return schedule;
+    }
+
+    fetchReservations = (range: {startTime: Moment, endTime: Moment}, resourceIds: number[]): Observable<IdlObject> => {
+        return this.pcrud.search('bresv', {
+            '-or': {'target_resource': resourceIds, 'current_resource': resourceIds},
+            'end_time': {'>': range.startTime.toISOString()},
+            'start_time': {'<': range.endTime.toISOString()},
+            'return_time': null,
+            'cancel_time': null },
+            {'flesh': 1, 'flesh_fields': {'bresv': ['current_resource', 'usr']}});
+    }
+
+    addReservationToSchedule = (reservation: IdlObject, schedule: ScheduleRow[], granularity: number, timezone: string): ScheduleRow[] => {
+        for (let index = 0; index < schedule.length; index++) {
+            const start = schedule[index].time;
+            const end = (index + 1 < schedule.length) ?
+                schedule[index + 1].time :
+                schedule[index].time.clone().add(granularity, 'minutes');
+            if ((Moment.tz(reservation.start_time(), timezone).isBefore(end)) &&
+                (Moment.tz(reservation.end_time(), timezone).isAfter(start))) {
+                if (!schedule[index][reservation.current_resource().barcode()]) {
+                    schedule[index][reservation.current_resource().barcode()] = [];
+                }
+                if (schedule[index][reservation.current_resource().barcode()]
+                    .findIndex(patron => patron.patronId === reservation.usr().id()) === -1) {
+                    schedule[index][reservation.current_resource().barcode()].push(
+                        {'patronLabel': reservation.usr().usrname(),
+                        'patronId': reservation.usr().id(),
+                        'reservationId': reservation.id()});
+                }
+            }
+
+        }
+        return schedule;
+
+    }
+
+    // Evergreen uses its own day of week style, where dow_0 = Monday and dow_6 = Sunday
+    private evergreenStyleDow = (original: Date): string => {
+        const daysInAWeek = 7;
+        const offset = 6;
+        return 'dow_' + (original.getDay() + offset) % daysInAWeek;
+    }
+
+
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/schedule-grid.spec.ts b/Open-ILS/src/eg2/src/app/staff/booking/schedule-grid.spec.ts
new file mode 100644
index 0000000000..85b567e73b
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/booking/schedule-grid.spec.ts
@@ -0,0 +1,51 @@
+import { TestBed } from '@angular/core/testing';
+import { AuthService } from '@eg/core/auth.service';
+import { PcrudService } from '@eg/core/pcrud.service';
+import { ScheduleGridService, ScheduleRow } from './schedule-grid.service';
+import * as Moment from 'moment-timezone';
+
+describe('ScheduleGridService', () => {
+    let service: ScheduleGridService;
+    beforeEach(() => {
+        const authServiceStub = {};
+        const pcrudServiceStub = {};
+        TestBed.configureTestingModule({
+            providers: [
+                ScheduleGridService,
+                { provide: AuthService, useValue: authServiceStub },
+                { provide: PcrudService, useValue: pcrudServiceStub }
+            ]
+        });
+        service = TestBed.get(ScheduleGridService);
+    });
+
+    it('should recognize when a row is completely busy', () => {
+        const busyRow: ScheduleRow = {
+            'time': Moment(),
+            'barcode1': [{patronLabel: 'Joe', patronId: 1, reservationId: 3}],
+            'barcode2': [{patronLabel: 'Jill', patronId: 2, reservationId: 5}],
+            'barcode3': [{patronLabel: 'James', patronId: 3, reservationId: 12},
+                {patronLabel: 'Juanes', patronId: 4, reservationId: 18}]
+        };
+        expect(service.resourceAvailabilityIcon(busyRow, 3).icon).toBe('event_busy');
+    });
+
+    it('should recognize when a row has some availability', () => {
+        const rowWithAvailability: ScheduleRow = {
+            'time': Moment(),
+            'barcode3': [{patronLabel: 'James', patronId: 3, reservationId: 11},
+                {patronLabel: 'Juanes', patronId: 4, reservationId: 17}]
+        };
+        expect(service.resourceAvailabilityIcon(rowWithAvailability, 3).icon).toBe('event_available');
+    });
+
+    it('should recognize 4 February 2019 as a Monday', () => {
+        const date = new Date(2019, 1, 4);
+        expect(service['evergreenStyleDow'](date)).toBe('dow_0');
+    });
+
+    it('should recognize 3 February 2019 as a Sunday', () => {
+        const date = new Date(2019, 1, 3);
+        expect(service['evergreenStyleDow'](date)).toBe('dow_6');
+    });
+});
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.html
index 3c09dded91..bcd980e45f 100644
--- a/Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.html
@@ -130,6 +130,11 @@
       i18n-group group="Booking" i18n-label label="Make Items Bookable"
       (onClick)="makeBookable($event)">
     </eg-grid-toolbar-action>
+
+    <eg-grid-toolbar-action
+      i18n-group group="Booking" i18n-label label="Manage Reservations"
+      (onClick)="manageReservations($event)">
+    </eg-grid-toolbar-action>
     
     <!-- row actions: Edit -->
 
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.ts
index 25e0894a0e..edde96cbc5 100644
--- a/Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.ts
@@ -871,7 +871,7 @@ export class HoldingsMaintenanceComponent implements OnInit {
     bookItems(rows: HoldingsEntry[]) {
         const copyIds = this.selectedCopyIds(rows);
         if (copyIds.length > 0) {
-            alert('TODO');
+            this.router.navigate(['staff', 'booking', 'create_reservation', 'for_resource', rows.filter(r => Boolean(r.copy))[0].copy.barcode()]);
         }
     }
 
@@ -882,4 +882,11 @@ export class HoldingsMaintenanceComponent implements OnInit {
             this.makeBookableDialog.open({});
         }
     }
+
+    manageReservations(rows: HoldingsEntry[]) {
+        const copyIds = this.selectedCopyIds(rows);
+        if (copyIds.length > 0) {
+            this.router.navigate(['staff', 'booking', 'manage_reservations', 'by_resource', rows.filter(r => Boolean(r.copy))[0].copy.barcode()]);
+        }
+    }
 }
diff --git a/Open-ILS/src/eg2/src/app/staff/common.module.ts b/Open-ILS/src/eg2/src/app/staff/common.module.ts
index e833a347e2..458b0f5ffb 100644
--- a/Open-ILS/src/eg2/src/app/staff/common.module.ts
+++ b/Open-ILS/src/eg2/src/app/staff/common.module.ts
@@ -17,8 +17,9 @@ import {TranslateComponent} from '@eg/staff/share/translate/translate.component'
 import {AdminPageComponent} from '@eg/staff/share/admin-page/admin-page.component';
 import {EgHelpPopoverComponent} from '@eg/share/eg-help-popover/eg-help-popover.component';
 import {DatetimeValidatorDirective} from '@eg/share/validators/datetime_validator.directive';
-import {ReactiveFormsModule} from '@angular/forms';
 import {MultiSelectComponent} from '@eg/share/multi-select/multi-select.component';
+import {NotBeforeMomentValidatorDirective} from '@eg/share/validators/not_before_moment_validator.directive';
+import {PatronBarcodeValidatorDirective} from '@eg/share/validators/patron_barcode_validator.directive';
 
 /**
  * Imports the EG common modules and adds modules common to all staff UI's.
@@ -39,7 +40,9 @@ import {MultiSelectComponent} from '@eg/share/multi-select/multi-select.componen
     AdminPageComponent,
     EgHelpPopoverComponent,
     DatetimeValidatorDirective,
-    MultiSelectComponent
+    MultiSelectComponent,
+    NotBeforeMomentValidatorDirective,
+    PatronBarcodeValidatorDirective,
   ],
   imports: [
     EgCommonModule,
@@ -63,7 +66,9 @@ import {MultiSelectComponent} from '@eg/share/multi-select/multi-select.componen
     AdminPageComponent,
     EgHelpPopoverComponent,
     DatetimeValidatorDirective,
-    MultiSelectComponent
+    MultiSelectComponent,
+    NotBeforeMomentValidatorDirective,
+    PatronBarcodeValidatorDirective
   ]
 })
 
diff --git a/Open-ILS/src/eg2/src/app/staff/nav.component.html b/Open-ILS/src/eg2/src/app/staff/nav.component.html
index 3595b3c9be..2be451825e 100644
--- a/Open-ILS/src/eg2/src/app/staff/nav.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/nav.component.html
@@ -310,11 +310,11 @@
           Booking
         </a>
         <div class="dropdown-menu" ngbDropdownMenu>
-          <a class="dropdown-item" href="/eg/staff/booking/legacy/booking/reservation">
+          <a class="dropdown-item" href="staff/booking/create_reservation">
             <span class="material-icons">add</span>
             <span i18n>Create Reservations</span>
           </a>
-          <a class="dropdown-item" href="/eg/staff/booking/legacy/booking/pull_list">
+          <a class="dropdown-item" href="staff/booking/pull_list">
             <span class="material-icons">list</span>
             <span i18n>Pull List</span>
           </a>
@@ -322,14 +322,18 @@
             <span class="material-icons">pin_drop</span>
             <span i18n>Capture Resources</span>
           </a>
-          <a class="dropdown-item" href="/eg/staff/booking/legacy/booking/pickup">
+          <a class="dropdown-item" href="staff/booking/pickup">
             <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>
+          <a class="dropdown-item" href="staff/booking/manage_reservations">
+            <span class="material-icons">edit_attributes</span>
+            <span i18n>Manage Reservations</span>
+          </a>
         </div>
       </div>
     </div>
diff --git a/Open-ILS/src/eg2/src/app/staff/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/routing.module.ts
index 6f20336660..e390a3db4d 100644
--- a/Open-ILS/src/eg2/src/app/staff/routing.module.ts
+++ b/Open-ILS/src/eg2/src/app/staff/routing.module.ts
@@ -19,6 +19,9 @@ const routes: Routes = [{
     redirectTo: 'splash',
     pathMatch: 'full',
   }, {
+    path: 'booking',
+    loadChildren : '@eg/staff/booking/booking.module#BookingModule'
+  }, {
     path: 'about',
     component: AboutComponent
   }, {
diff --git a/Open-ILS/src/eg2/src/app/staff/share/patron.service.ts b/Open-ILS/src/eg2/src/app/staff/share/patron.service.ts
new file mode 100644
index 0000000000..b11626c71d
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/patron.service.ts
@@ -0,0 +1,23 @@
+import {Injectable} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {Observable} from 'rxjs';
+
+
+ at Injectable()
+export class PatronService {
+    constructor(
+        private net: NetService,
+        private auth: AuthService
+    ) {}
+
+    bcSearch(barcode: string): Observable<any> {
+        return this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.get_barcodes',
+            this.auth.token(), this.auth.user().ws_ou(),
+           'actor', barcode);
+    }
+
+}
+
diff --git a/Open-ILS/src/eg2/src/polyfills.ts b/Open-ILS/src/eg2/src/polyfills.ts
index 9dc048b83f..1e3dcd8664 100644
--- a/Open-ILS/src/eg2/src/polyfills.ts
+++ b/Open-ILS/src/eg2/src/polyfills.ts
@@ -37,6 +37,7 @@
 // PhantomJS needs these
 import 'core-js/es6/array';
 import 'core-js/es6/string';
+import 'core-js/es6/symbol'; // needed by app/staff/booking/reservation-actions.spec.ts
 
 /** IE10 and IE11 requires the following for NgClass support on SVG elements */
 // import 'classlist.js';  // Run `npm install --save classlist.js`.
diff --git a/Open-ILS/src/eg2/src/styles.css b/Open-ILS/src/eg2/src/styles.css
index 9573b52e4a..ef97e2a93d 100644
--- a/Open-ILS/src/eg2/src/styles.css
+++ b/Open-ILS/src/eg2/src/styles.css
@@ -13,7 +13,15 @@ body, .form-control, .btn, .input-group-text {
    */
   font-size: .88rem;
 }
-h2 {font-size: 1.25rem}
+h2 {
+  font-size: 1.25rem;
+  font-weight: 550;
+  color: #129a78; /* official color of the Evergreen logo */
+  text-decoration: underline #129a78;
+}
+h2.card-header {
+  text-decoration: none;
+}
 h3 {font-size: 1.15rem}
 h4 {font-size: 1.05rem}
 h5 {font-size: .95rem}
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Booking.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Booking.pm
index a715f88259..c01db43442 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Booking.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Booking.pm
@@ -192,7 +192,7 @@ __PACKAGE__->register_method(
 sub create_bresv {
     my ($self, $client, $authtoken,
         $target_user_barcode, $datetime_range, $pickup_lib,
-        $brt, $brsrc_list, $attr_values, $email_notify) = @_;
+        $brt, $brsrc_list, $attr_values, $email_notify, $note) = @_;
 
     $brsrc_list = [ undef ] if not defined $brsrc_list;
     return undef if scalar(@$brsrc_list) < 1; # Empty list not ok.
@@ -213,6 +213,7 @@ sub create_bresv {
         $bresv->start_time($datetime_range->[0]);
         $bresv->end_time($datetime_range->[1]);
         $bresv->email_notify(1) if $email_notify;
+        $bresv->note($note) if $note;
 
         # A little sanity checking: don't agree to put a reservation on a
         # brsrc and a brt when they don't match.  In fact, bomb out of
@@ -306,6 +307,7 @@ __PACKAGE__->register_method(
             {type => 'list', desc => 'Booking resource (undef ok; empty not ok)'},
             {type => 'array', desc => 'Attribute values selected'},
             {type => 'bool', desc => 'Email notification?'},
+            {type => 'string', desc => 'Optional note'},
         ],
         return => { desc => "A hash containing the new bresv and a list " .
             "of new bravm"}
diff --git a/Open-ILS/src/sql/Pg/095.schema.booking.sql b/Open-ILS/src/sql/Pg/095.schema.booking.sql
index 974f3b9bba..7144fded4e 100644
--- a/Open-ILS/src/sql/Pg/095.schema.booking.sql
+++ b/Open-ILS/src/sql/Pg/095.schema.booking.sql
@@ -129,7 +129,8 @@ CREATE TABLE booking.reservation (
 	                                DEFERRABLE INITIALLY DEFERRED,
 	capture_staff    INT            REFERENCES actor.usr(id)
 	                                DEFERRABLE INITIALLY DEFERRED,
-	email_notify     BOOLEAN        NOT NULL DEFAULT FALSE
+	email_notify     BOOLEAN        NOT NULL DEFAULT FALSE,
+	note             TEXT
 ) INHERITS (money.billable_xact);
 
 ALTER TABLE booking.reservation ADD PRIMARY KEY (id);
diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index f2c6e85de2..36260a4f4c 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -20133,3 +20133,91 @@ $TEMPLATE$
 
 -- Allow for 1k stock templates
 SELECT SETVAL('config.print_template_id_seq'::TEXT, 1000);
+
+INSERT INTO config.workstation_setting_type
+    (name, grp, datatype, label)
+VALUES (
+    'eg.grid.circ.patron.group_members', 'gui', 'object',
+    oils_i18n_gettext(
+    'eg.grid.circ.patron.group_members',
+    'Grid Config: circ.patron.group_members',
+    'cwst', 'label')
+);
+
+INSERT INTO config.workstation_setting_type (name,label,grp,datatype)
+VALUES ('eg.circ.bills.annotatepayment','Bills: Annotate Payment', 'circ', 'bool');
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.grid.booking.manage', 'gui', 'object',
+    oils_i18n_gettext(
+        'booking.manage',
+        'Grid Config: Booking Manage Reservations',
+        'cwst', 'label')
+), (
+    'eg.grid.booking.pickup.ready', 'gui', 'object',
+    oils_i18n_gettext(
+        'booking.pickup.ready',
+        'Grid Config: Booking Ready to pick up grid',
+        'cwst', 'label')
+), (
+    'eg.grid.booking.pickup.picked_up', 'gui', 'object',
+    oils_i18n_gettext(
+        'booking.pickup.picked_up',
+        'Grid Config: Booking Already Picked Up grid',
+        'cwst', 'label')
+), (
+    'eg.grid.booking.return.patron.picked_up', 'gui', 'object',
+    oils_i18n_gettext(
+        'booking.return.patron.picked_up',
+        'Grid Config: Booking Return Patron tab Already Picked Up grid',
+        'cwst', 'label')
+), (
+    'eg.grid.booking.return.patron.returned', 'gui', 'object',
+    oils_i18n_gettext(
+        'booking.return.patron.returned',
+        'Grid Config: Booking Return Patron tab Returned Today grid',
+        'cwst', 'label')
+), (
+    'eg.grid.booking.return.resource.picked_up', 'gui', 'object',
+    oils_i18n_gettext(
+        'booking.return.resourcce.picked_up',
+        'Grid Config: Booking Return Resource tab Already Picked Up grid',
+        'cwst', 'label')
+), (
+    'eg.grid.booking.return.resource.returned', 'gui', 'object',
+    oils_i18n_gettext(
+        'booking.return.resource.returned',
+        'Grid Config: Booking Return Resource tab Returned Today grid',
+        'cwst', 'label')
+), (
+    'eg.booking.manage.selected_org_family', 'gui', 'object',
+    oils_i18n_gettext(
+        'booking.manage.selected_org_family',
+        'Sticky setting for pickup ou family in Manage Reservations screen',
+        '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.create.granularity', 'gui', 'integer',
+    oils_i18n_gettext(
+        'booking.create.granularity',
+        'Sticky setting for granularity combobox in Booking Create',
+        'cwst', 'label')
+), (
+    'eg.booking.create.multiday', 'gui', 'bool',
+    oils_i18n_gettext(
+        'booking.create.multiday',
+        'Default to creating multiday booking reservations',
+        'cwst', 'label')
+), (
+    'eg.booking.pickup.ready.only_show_captured', 'gui', 'bool',
+    oils_i18n_gettext(
+        'booking.pickup.ready.only_show_captured',
+        'Include only resources that have been captured in the Ready grid in the Pickup screen',
+        'cwst', 'label')
+);
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.booking-sticky-settings.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.booking-sticky-settings.sql
new file mode 100644
index 0000000000..8da02ed13b
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.booking-sticky-settings.sql
@@ -0,0 +1,78 @@
+BEGIN;
+--SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.grid.booking.manage', 'gui', 'object',
+    oils_i18n_gettext(
+        'booking.manage',
+        'Grid Config: Booking Manage Reservations',
+        'cwst', 'label')
+), (
+    'eg.grid.booking.pickup.ready', 'gui', 'object',
+    oils_i18n_gettext(
+        'booking.pickup.ready',
+        'Grid Config: Booking Ready to pick up grid',
+        'cwst', 'label')
+), (
+    'eg.grid.booking.pickup.picked_up', 'gui', 'object',
+    oils_i18n_gettext(
+        'booking.pickup.picked_up',
+        'Grid Config: Booking Already Picked Up grid',
+        'cwst', 'label')
+), (
+    'eg.grid.booking.return.patron.picked_up', 'gui', 'object',
+    oils_i18n_gettext(
+        'booking.return.patron.picked_up',
+        'Grid Config: Booking Return Patron tab Already Picked Up grid',
+        'cwst', 'label')
+), (
+    'eg.grid.booking.return.patron.returned', 'gui', 'object',
+    oils_i18n_gettext(
+        'booking.return.patron.returned',
+        'Grid Config: Booking Return Patron tab Returned Today grid',
+        'cwst', 'label')
+), (
+    'eg.grid.booking.return.resource.picked_up', 'gui', 'object',
+    oils_i18n_gettext(
+        'booking.return.resourcce.picked_up',
+        'Grid Config: Booking Return Resource tab Already Picked Up grid',
+        'cwst', 'label')
+), (
+    'eg.grid.booking.return.resource.returned', 'gui', 'object',
+    oils_i18n_gettext(
+        'booking.return.resource.returned',
+        'Grid Config: Booking Return Resource tab Returned Today grid',
+        'cwst', 'label')
+), (
+    'eg.booking.manage.selected_org_family', 'gui', 'object',
+    oils_i18n_gettext(
+        'booking.manage.selected_org_family',
+        'Sticky setting for pickup ou family in Manage Reservations screen',
+        '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.create.granularity', 'gui', 'integer',
+    oils_i18n_gettext(
+        'booking.create.granularity',
+        'Sticky setting for granularity combobox in Booking Create',
+        'cwst', 'label')
+), (
+    'eg.booking.create.multiday', 'gui', 'bool',
+    oils_i18n_gettext(
+        'booking.create.multiday',
+        'Default to creating multiday booking reservations',
+        'cwst', 'label')
+), (
+    'eg.booking.pickup.ready.only_show_captured', 'gui', 'bool',
+    oils_i18n_gettext(
+        'booking.pickup.ready.only_show_captured',
+        'Include only resources that have been captured in the Ready grid in the Pickup screen',
+        'cwst', 'label')
+);
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.add_note_bresv.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.add_note_bresv.sql
new file mode 100644
index 0000000000..4742f1d9a4
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.add_note_bresv.sql
@@ -0,0 +1,6 @@
+BEGIN;
+
+ALTER TABLE booking.reservation
+    ADD COLUMN note TEXT;
+
+COMMIT;
diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
index d30ad1ab67..2cddd413ef 100644
--- a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
+++ b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
@@ -45,6 +45,9 @@
     <eg-grid-action handler="book_copies_now"
       disabled="need_one_selected"
       label="[% l('Book Item Now') %]"></eg-grid-action>
+    <eg-grid-action handler="manage_reservations"
+      disabled="need_one_selected"
+      label="[% l('Manage Reservations') %]"></eg-grid-action>
     <eg-grid-action handler="requestItems"
       label="[% l('Request Items') %]"></eg-grid-action>
     <eg-grid-action handler="attach_to_peer_bib"
diff --git a/Open-ILS/src/templates/staff/cat/item/index.tt2 b/Open-ILS/src/templates/staff/cat/item/index.tt2
index 785f250901..6af18a63dd 100644
--- a/Open-ILS/src/templates/staff/cat/item/index.tt2
+++ b/Open-ILS/src/templates/staff/cat/item/index.tt2
@@ -89,6 +89,7 @@
         <li><a href ng-click="show_in_catalog()">[% l('Show in Catalog') %]</a></li>
         <li><a href ng-click="make_copies_bookable()">[% l('Make Items Bookable') %]</a></li>
         <li><a href ng-click="book_copies_now()">[% l('Book Item Now') %]</a></li>
+        <li><a href ng-click="manage_reservations()">[% l('Manage Reservations') %]</a></li>
         <li><a href ng-click="requestItems()">[% l('Request Items') %]</a></li>
         <li><a href ng-click="attach_to_peer_bib()">[% l('Link as Conjoined to Previously Marked Bib Record') %]</a></li>
         <li><a href ng-click="selectedHoldingsCopyDelete()">[% l('Delete Items') %]</a></li>
diff --git a/Open-ILS/src/templates/staff/cat/item/t_list.tt2 b/Open-ILS/src/templates/staff/cat/item/t_list.tt2
index 024271e767..3747835a8e 100644
--- a/Open-ILS/src/templates/staff/cat/item/t_list.tt2
+++ b/Open-ILS/src/templates/staff/cat/item/t_list.tt2
@@ -19,6 +19,9 @@
   <eg-grid-action handler="book_copies_now"
     disabled="need_one_selected"
     label="[% l('Book Item Now') %]"></eg-grid-action>
+  <eg-grid-action handler="manage_reservations"
+    disabled="need_one_selected"
+    label="[% l('Manage Reservations') %]"></eg-grid-action>
   <eg-grid-action handler="requestItems"
     label="[% l('Request Items') %]"></eg-grid-action>
   <eg-grid-action handler="attach_to_peer_bib"
diff --git a/Open-ILS/src/templates/staff/circ/patron/index.tt2 b/Open-ILS/src/templates/staff/circ/patron/index.tt2
index 259dae7019..677a4e8471 100644
--- a/Open-ILS/src/templates/staff/circ/patron/index.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/index.tt2
@@ -215,17 +215,22 @@ angular.module('egCoreMod').run(['egStrings', function(s) {
             </a>
           </li>
           <li>
-            <a href="./booking/legacy/booking/reservation?patron_barcode={{patron().card().barcode()}}" target="_top">
-              [% l('Booking: Create or Cancel Reservations') %]
+            <a href="/eg2/staff/booking/manage_reservations/by_patron/{{patron().id()}}" target="_top">
+              [% l('Booking: Manage Reservations') %]
             </a>
           </li>
           <li>
-            <a href="./booking/legacy/booking/pickup?patron_barcode={{patron().card().barcode()}}" target="_top">
+            <a href="/eg2/staff/booking/create_reservation/for_patron/{{patron().id()}}" target="_top">
+              [% l('Booking: Create Reservation') %]
+            </a>
+          </li>
+          <li>
+            <a href="/eg2/staff/booking/pickup/by_patron/{{patron().id()}}" target="_top">
               [% l('Booking: Pick Up Reservations') %]
             </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>
diff --git a/Open-ILS/src/templates/staff/navbar.tt2 b/Open-ILS/src/templates/staff/navbar.tt2
index a9208c8d04..1028f42bb7 100644
--- a/Open-ILS/src/templates/staff/navbar.tt2
+++ b/Open-ILS/src/templates/staff/navbar.tt2
@@ -439,13 +439,13 @@
         </a>
         <ul uib-dropdown-menu>
           <li>
-            <a href="./booking/legacy/booking/reservation" target="_self">
+            <a href="/eg2/staff/booking/create_reservation" target="_self">
               <span class="glyphicon glyphicon-plus"></span>
               [% l('Create Reservations') %]
             </a>
           </li>
           <li>
-            <a href="./booking/legacy/booking/pull_list" target="_self">
+            <a href="/eg2/staff/booking/pull_list" target="_self">
               <span class="glyphicon glyphicon-th-list"></span>
               [% l('Pull List') %]
             </a>
@@ -457,17 +457,23 @@
             </a>
           </li>
           <li>
-            <a href="./booking/legacy/booking/pickup" target="_self">
+            <a href="/eg2/staff/booking/pickup" target="_self">
               <span class="glyphicon glyphicon-export"></span>
               [% l('Pick Up Reservations') %]
             </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>
           </li>
+          <li>
+            <a href="/eg2/staff/booking/manage_reservations" target="_self">
+              <span class="glyphicon glyphicon-wrench"></span>
+              [% l('Manage Reservations') %]
+            </a>
+          </li>
         </ul>
       </li>
 
diff --git a/Open-ILS/web/js/ui/default/booking/capture.js b/Open-ILS/web/js/ui/default/booking/capture.js
index 0e69a2dc7a..7a53625734 100644
--- a/Open-ILS/web/js/ui/default/booking/capture.js
+++ b/Open-ILS/web/js/ui/default/booking/capture.js
@@ -76,6 +76,13 @@ CaptureDisplay.prototype._generate_route_line = function(payload) {
     div.appendChild(strong);
     return div;
 };
+CaptureDisplay.prototype._generate_notes_line = function(payload) {
+    var p = document.createElement("p");
+    if (payload.reservation.note()) {
+        p.innerHTML = "<strong>" + payload.reservation.note() + "</strong>";
+    }
+    return p;
+};
 CaptureDisplay.prototype._generate_patron_info = function(payload) {
     var p = document.createElement("p");
     p.innerHTML = "<strong>" + localeStrings.RESERVED + "</strong> " +
@@ -131,6 +138,8 @@ CaptureDisplay.prototype.display_with_transit_info = function(result) {
     p.appendChild(this._generate_author_line(result.payload));
     div.appendChild(p);
 
+    div.appendChild(this._generate_notes_line(result.payload));
+
     div.appendChild(this._generate_patron_info(result.payload));
     div.appendChild(this._generate_resv_info(result.payload));
     div.appendChild(this._generate_meta_info(result));
diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
index c207b9797a..f474dc4401 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
@@ -936,75 +936,10 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         });
     }
 
-    $scope.book_copies_now = function() {
-        var copies_by_record = {};
-        var record_list = [];
-        angular.forEach(
-            $scope.holdingsGridControls.selectedItems(),
-            function (item) {
-                var record_id = item['call_number.record.id'];
-                if (typeof copies_by_record[ record_id ] == 'undefined') {
-                    copies_by_record[ record_id ] = [];
-                    record_list.push( record_id );
-                }
-                copies_by_record[ record_id ].push(item.id);
-            }
-        );
-
-        var promises = [];
-        var combined_brt = [];
-        var combined_brsrc = [];
-        angular.forEach(record_list, function(record_id) {
-            promises.push(
-                egCore.net.request(
-                    'open-ils.booking',
-                    'open-ils.booking.resources.create_from_copies',
-                    egCore.auth.token(),
-                    copies_by_record[record_id]
-                ).then(function(results) {
-                    if (results && results['brt']) {
-                        combined_brt = combined_brt.concat(results['brt']);
-                    }
-                    if (results && results['brsrc']) {
-                        combined_brsrc = combined_brsrc.concat(results['brsrc']);
-                    }
-                })
-            );
-        });
-
-        $q.all(promises).then(function() {
-            if (combined_brt.length > 0 || combined_brsrc.length > 0) {
-                $uibModal.open({
-                    template: '<eg-embed-frame url="booking_admin_url" handlers="funcs"></eg-embed-frame>',
-                    backdrop: 'static',
-                    animation: true,
-                    size: 'md',
-                    controller:
-                           ['$scope','$location','egCore','$uibModalInstance',
-                    function($scope , $location , egCore , $uibModalInstance) {
-
-                        $scope.funcs = {
-                            ses : egCore.auth.token(),
-                            bresv_interface_opts : {
-                                booking_results : {
-                                     brt : combined_brt
-                                    ,brsrc : combined_brsrc
-                                }
-                            }
-                        }
-
-                        var booking_path = '/eg/booking/reservation';
-
-                        $scope.booking_admin_url =
-                            $location.absUrl().replace(/\/eg\/staff.*/, booking_path);
-
-                    }]
-                });
-            }
-        });
+    $scope.book_copies_now = function(items) {
+        location.href = "/eg2/staff/booking/create_reservation/for_resource/" + items[0]['barcode'];
     }
 
-
     $scope.requestItems = function() {
         var copy_list = gatherSelectedHoldingsIds();
         if (copy_list.length == 0) return;
@@ -1074,6 +1009,13 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         });
     }
 
+    $scope.manage_reservations = function() {
+        var item = $scope.holdingsGridControls.selectedItems()[0];
+        if (item)
+            location.href = "/eg2/staff/booking/manage_reservations/by_resource/" + item.barcode;
+    }
+
+
     $scope.view_place_orders = function() {
         if (!$scope.record_id) return;
         var url = egCore.env.basePath + 'acq/legacy/lineitem/related/' + $scope.record_id + '?target=bib';
diff --git a/Open-ILS/web/js/ui/default/staff/cat/item/app.js b/Open-ILS/web/js/ui/default/staff/cat/item/app.js
index 5e418e76e4..b861801746 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/item/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/item/app.js
@@ -103,10 +103,7 @@ function($scope , $q , $window , $location , $timeout , egCore , egNet , egGridD
     }
 
     $scope.book_copies_now = function() {
-        itemSvc.book_copies_now([{
-            id : $scope.args.copyId,
-            'call_number.record.id' : $scope.args.recordId
-        }]);
+        itemSvc.book_copies_now([$scope.args.copyBarcode]);
     }
 
     $scope.findAcquisition = function() {
@@ -144,6 +141,10 @@ function($scope , $q , $window , $location , $timeout , egCore , egNet , egGridD
         });
     }
 
+    $scope.manage_reservations = function() {
+        itemSvc.manage_reservations([$scope.args.copyBarcode]);
+    }
+
     $scope.requestItems = function() {
         itemSvc.requestItems([$scope.args.copyId]);
     }
@@ -524,7 +525,15 @@ function($scope , $q , $window , $location , $timeout , egCore , egNet , egGridD
     }
 
     $scope.book_copies_now = function() {
-        itemSvc.book_copies_now(copyGrid.selectedItems());
+        var item = copyGrid.selectedItems()[0];
+        if (item)
+            itemSvc.book_copies_now(item.barcode);
+    }
+
+    $scope.manage_reservations = function() {
+        var item = copyGrid.selectedItems()[0];
+        if (item)
+            itemSvc.manage_reservations(item.barcode);
     }
 
     $scope.requestItems = function() {
diff --git a/Open-ILS/web/js/ui/default/staff/circ/services/item.js b/Open-ILS/web/js/ui/default/staff/circ/services/item.js
index 6382852d32..eda4d8e07b 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/services/item.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/services/item.js
@@ -350,72 +350,12 @@ function(egCore , egCirc , $uibModal , $q , $timeout , $window , egConfirmDialog
         });
     }
 
-    service.book_copies_now = function(items) {
-        var copies_by_record = {};
-        var record_list = [];
-        angular.forEach(
-            items,
-            function (item) {
-                var record_id = item['call_number.record.id'];
-                if (typeof copies_by_record[ record_id ] == 'undefined') {
-                    copies_by_record[ record_id ] = [];
-                    record_list.push( record_id );
-                }
-                copies_by_record[ record_id ].push(item.id);
-            }
-        );
-
-        var promises = [];
-        var combined_brt = [];
-        var combined_brsrc = [];
-        angular.forEach(record_list, function(record_id) {
-            promises.push(
-                egCore.net.request(
-                    'open-ils.booking',
-                    'open-ils.booking.resources.create_from_copies',
-                    egCore.auth.token(),
-                    copies_by_record[record_id]
-                ).then(function(results) {
-                    if (results && results['brt']) {
-                        combined_brt = combined_brt.concat(results['brt']);
-                    }
-                    if (results && results['brsrc']) {
-                        combined_brsrc = combined_brsrc.concat(results['brsrc']);
-                    }
-                })
-            );
-        });
-
-        $q.all(promises).then(function() {
-            if (combined_brt.length > 0 || combined_brsrc.length > 0) {
-                $uibModal.open({
-                    template: '<eg-embed-frame url="booking_admin_url" handlers="funcs"></eg-embed-frame>',
-                    backdrop: 'static',
-                    animation: true,
-                    size: 'md',
-                    controller:
-                           ['$scope','$location','egCore','$uibModalInstance',
-                    function($scope , $location , egCore , $uibModalInstance) {
-
-                        $scope.funcs = {
-                            ses : egCore.auth.token(),
-                            bresv_interface_opts : {
-                                booking_results : {
-                                     brt : combined_brt
-                                    ,brsrc : combined_brsrc
-                                }
-                            }
-                        }
-
-                        var booking_path = '/eg/booking/reservation';
-
-                        $scope.booking_admin_url =
-                            $location.absUrl().replace(/\/eg\/staff.*/, booking_path);
+    service.book_copies_now = function(barcode) {
+        location.href = "/eg2/staff/booking/create_reservation/for_resource/" + barcode;
+    }
 
-                    }]
-                });
-            }
-        });
+    service.manage_reservations = function(barcode) {
+        location.href = "/eg2/staff/booking/manage_reservations/by_resource/" + barcode;
     }
 
     service.requestItems = function(copy_list) {

commit de4497f46097984573c808a36eef780eb35da1bd
Author: Jane Sandberg <sandbej at linnbenton.edu>
Date:   Thu Jul 25 10:13:34 2019 -0700

    LP1816475: Fixes incorrect IDL relationship for bresv pickup_lib
    
    Many thanks to Dan Wells for pointing out this issue.
    
    Signed-off-by: Jane Sandberg <sandbej at linnbenton.edu>
    Signed-off-by: Christine Burns <christine.burns at bc.libraries.coop>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index 11839cf2fe..de6933f6f7 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -5167,7 +5167,7 @@ SELECT  usr,
 			<link field="target_resource" reltype="has_a" key="id" map="" class="brsrc"/>
 			<link field="current_resource" reltype="has_a" key="id" map="" class="brsrc"/>
 			<link field="request_lib" reltype="has_a" key="id" map="" class="aou"/>
-			<link field="pickup_lib" reltype="might_have" key="id" map="" class="aou"/>
+			<link field="pickup_lib" reltype="has_a" key="id" map="" class="aou"/>
 			<link field="capture_staff" reltype="might_have" key="id" map="" class="au"/>
 			<link field="attr_val_maps" reltype="has_many" key="reservation" map="" class="bravm"/>
 		</links>

-----------------------------------------------------------------------

Summary of changes:
 Open-ILS/examples/fm_IDL.xml                       |  13 +-
 .../app/share/fm-editor/fm-editor.component.html   |  19 +
 .../src/app/share/fm-editor/fm-editor.component.ts |  24 +-
 .../src/app/share/grid/grid-column.component.ts    |   4 +
 .../src/app/share/grid/grid-toolbar.component.html |   5 +-
 .../src/eg2/src/app/share/grid/grid.component.html |   2 +-
 .../src/eg2/src/app/share/grid/grid.component.ts   |   9 +-
 Open-ILS/src/eg2/src/app/share/grid/grid.ts        |   4 +-
 .../not_before_moment_validator.directive.ts       |  32 ++
 .../patron_barcode_validator.directive.spec.ts     |  43 +++
 .../patron_barcode_validator.directive.ts          |  56 +++
 .../eg2/src/app/staff/booking/booking.module.ts    |  42 +++
 .../booking_resource_validator.directive.ts        |  42 +++
 .../booking/cancel-reservation-dialog.component.ts |  63 ++++
 .../create-reservation-dialog.component.html       |  87 +++++
 .../booking/create-reservation-dialog.component.ts | 212 +++++++++++
 .../booking/create-reservation.component.html      | 218 +++++++++++
 .../staff/booking/create-reservation.component.ts  | 420 +++++++++++++++++++++
 .../booking/manage-reservations.component.html     |  72 ++++
 .../staff/booking/manage-reservations.component.ts | 188 +++++++++
 .../staff/booking/no-timezone-set.component.html   |  17 +
 .../app/staff/booking/no-timezone-set.component.ts |  16 +
 .../src/app/staff/booking/pickup.component.html    |  27 ++
 .../eg2/src/app/staff/booking/pickup.component.ts  | 110 ++++++
 .../src/app/staff/booking/pull-list.component.html |  47 +++
 .../src/app/staff/booking/pull-list.component.ts   | 127 +++++++
 .../staff/booking/reservation-actions.service.ts   |  32 ++
 .../app/staff/booking/reservation-actions.spec.ts  |  35 ++
 .../staff/booking/reservations-grid.component.html |  68 ++++
 .../staff/booking/reservations-grid.component.ts   | 306 +++++++++++++++
 .../src/app/staff/booking/return.component.html    |  46 +++
 .../eg2/src/app/staff/booking/return.component.ts  | 145 +++++++
 .../eg2/src/app/staff/booking/routing.module.ts    |  44 +++
 .../src/app/staff/booking/schedule-grid.service.ts | 173 +++++++++
 .../src/app/staff/booking/schedule-grid.spec.ts    |  51 +++
 .../staff/catalog/record/holdings.component.html   |   5 +
 .../app/staff/catalog/record/holdings.component.ts |  13 +-
 Open-ILS/src/eg2/src/app/staff/common.module.ts    |  11 +-
 Open-ILS/src/eg2/src/app/staff/nav.component.html  |  12 +-
 Open-ILS/src/eg2/src/app/staff/routing.module.ts   |   3 +
 .../src/eg2/src/app/staff/share/patron.service.ts  |  23 ++
 Open-ILS/src/eg2/src/polyfills.ts                  |   1 +
 Open-ILS/src/eg2/src/styles.css                    |  10 +-
 .../perlmods/lib/OpenILS/Application/Booking.pm    |   4 +-
 Open-ILS/src/sql/Pg/002.schema.config.sql          |   2 +-
 Open-ILS/src/sql/Pg/095.schema.booking.sql         |   3 +-
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |  88 +++++
 .../sql/Pg/upgrade/1176.schema.add_note_bresv.sql  |   8 +
 .../upgrade/1177.data.booking-sticky-settings.sql  |  80 ++++
 .../src/templates/staff/cat/catalog/t_holdings.tt2 |   3 +
 Open-ILS/src/templates/staff/cat/item/index.tt2    |   1 +
 Open-ILS/src/templates/staff/cat/item/t_list.tt2   |   3 +
 Open-ILS/src/templates/staff/circ/patron/index.tt2 |  13 +-
 Open-ILS/src/templates/staff/navbar.tt2            |  14 +-
 Open-ILS/web/js/ui/default/booking/capture.js      |   9 +
 .../web/js/ui/default/staff/cat/catalog/app.js     |  76 +---
 Open-ILS/web/js/ui/default/staff/cat/item/app.js   |  19 +-
 .../web/js/ui/default/staff/circ/services/item.js  |  70 +---
 .../Circulation/booking-refresh.adoc               |  32 ++
 docs/circulation/booking.adoc                      | 271 +++++--------
 60 files changed, 3224 insertions(+), 349 deletions(-)
 create mode 100644 Open-ILS/src/eg2/src/app/share/validators/not_before_moment_validator.directive.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/validators/patron_barcode_validator.directive.spec.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/validators/patron_barcode_validator.directive.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/booking.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/booking_resource_validator.directive.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/cancel-reservation-dialog.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/manage-reservations.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/manage-reservations.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/no-timezone-set.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/no-timezone-set.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/pickup.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/pickup.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/pull-list.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/pull-list.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/reservation-actions.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/reservation-actions.spec.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/return.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/return.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/routing.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/schedule-grid.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/booking/schedule-grid.spec.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/patron.service.ts
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1176.schema.add_note_bresv.sql
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1177.data.booking-sticky-settings.sql
 create mode 100644 docs/RELEASE_NOTES_NEXT/Circulation/booking-refresh.adoc


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list