LP1816475: Turning Create Reservation controls into tabbed reactive form user/sandbergja/ng_bootstrap
authorJane Sandberg <sandbej@linnbenton.edu>
Sun, 2 Jun 2019 22:21:45 +0000 (15:21 -0700)
committerJane Sandberg <sandbej@linnbenton.edu>
Fri, 5 Jul 2019 03:15:34 +0000 (20:15 -0700)
Signed-off-by: Jane Sandberg <sandbej@linnbenton.edu>
Open-ILS/src/eg2/src/app/share/validators/patron_barcode_validator.directive.spec.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/validators/patron_barcode_validator.directive.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/booking/booking.module.ts
Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/booking/create-reservation-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.html
Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts
Open-ILS/src/eg2/src/app/staff/booking/manage-reservations.component.html
Open-ILS/src/eg2/src/app/staff/common.module.ts

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 (file)
index 0000000..1e1208e
--- /dev/null
@@ -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 (file)
index 0000000..81d1b15
--- /dev/null
@@ -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';
+
+@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' });
+            }
+        }));
+    }
+}
+
+@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);
+    }
+}
+
index df56a57..9ebce77 100644 (file)
@@ -3,6 +3,7 @@ import {ReactiveFormsModule} from '@angular/forms';
 import {StaffCommonModule} from '@eg/staff/common.module';
 import {BookingRoutingModule} from './routing.module';
 import {CreateReservationComponent} from './create-reservation.component';
+import {CreateReservationDialogComponent} from './create-reservation-dialog.component';
 import {ManageReservationsComponent} from './manage-reservations.component';
 import {OrgSelectWithDescendantsComponent} from './org-select-with-descendants.component';
 import {ReservationsGridComponent} from './reservations-grid.component';
@@ -23,6 +24,7 @@ import {PatronService} from '@eg/staff/share/patron.service';
     providers: [PatronService],
     declarations: [
         CreateReservationComponent,
+        CreateReservationDialogComponent,
         ManageReservationsComponent,
         NoTimezoneSetComponent,
         OrgSelectWithDescendantsComponent,
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 (file)
index 0000000..a1b40a5
--- /dev/null
@@ -0,0 +1,46 @@
+<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">&times;</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">
+    </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></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></eg-datetime-select>
+    </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 *ngIf="!(targetResource && targetResourceBarcode)"></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()" class="btn btn-info" i18n>Confirm reservation</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 (file)
index 0000000..0157bd9
--- /dev/null
@@ -0,0 +1,78 @@
+import {Component, Input, OnInit, ViewChild} from '@angular/core';
+import {FormGroup, FormControl, Validators} from '@angular/forms';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {AuthService} from '@eg/core/auth.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NetService} from '@eg/core/net.service';
+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 * as Moment from 'moment-timezone';
+
+@Component({
+  selector: 'eg-create-reservation-dialog',
+  templateUrl: './create-reservation-dialog.component.html'
+})
+
+export class CreateReservationDialogComponent
+    extends DialogComponent implements OnInit {
+
+    constructor(
+        private auth: AuthService,
+        private net: NetService,
+       private modal: NgbModal,
+       private pbv: PatronBarcodeValidator,
+        private toast: ToastService
+    ) {
+        super(modal);
+    }
+
+    create: FormGroup;
+
+    addBresv: () => void;
+
+    @Input() startTime: Moment;
+    @Input() endTime: Moment;
+    @Input() targetResource: number;
+    @Input() targetResourceBarcode: string;
+    @Input() attributes: any[];
+
+    @ViewChild('fail') private fail: AlertDialogComponent;
+
+    ngOnInit() {
+
+        this.create = new FormGroup({
+            'patronBarcode': new FormControl('',
+            [ Validators.required ],
+            [this.pbv.validate]
+            ),
+            'emailNotify': new FormControl(true),
+        });
+
+        this.addBresv = () => {
+            this.net.request(
+                'open-ils.booking',
+                'open-ils.booking.reservations.create',
+                this.auth.token(),
+               '99999382659', // patron barcode
+               ['2019-09-09 10:00', '2019-09-09 14:00'], // start/end
+               7, // pickup lib
+               555, // brt
+                this.targetResource ? [this.targetResource] : null,
+               [], // bravm
+               0 // email
+            ).subscribe(
+                (success) => {
+                   this.toast.success('Reservation successfully created');
+                    this.close();
+                }, (fail) => {
+                    console.warn(fail);
+                    this.fail.open();
+                }
+            );
+        }
+
+    }
+}
+
index b4d459b..e36922c 100644 (file)
 </eg-staff-banner>
 <eg-title i18n-prefix i18n-suffix prefix="Booking" suffix="Create Reservation"></eg-title>
 
-<div class="row">
-  <div class="col">
-    <eg-org-select-with-descendants labelText="Owning library" i18n-labelText (ouChange)="handleOwnerChange($event)">
-    </eg-org-select-with-descendants>
-  </div>
-  <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>
-      <div ngbDropdown>
-        <button *ngIf="!multiday" class="btn btn-outline-primary" ngbDropdownToggle><span class="material-icons">event</span><span i18n>Single day reservation</span></button>
-        <button *ngIf="multiday" class="btn btn-outline-primary" ngbDropdownToggle><span class="material-icons">date_range</span><span i18n>Multiple day reservation</span></button>
-        <div ngbDropdownMenu id="ideal-reservation-type">
-          <button (click)="handleSingleDayReservation()" class="btn btn-outline-primary" ngbDropdownItem><span class="material-icons">event</span><span i18n>Single day reservation</span></button>
-          <button (click)="handleMultiDayReservation()" class="btn btn-outline-primary" ngbDropdownItem><span class="material-icons">date_range</span><span i18n>Multiple day reservation</span></button>
+<form [formGroup]="criteria">
+<ngb-tabset>
+  <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">
+          <eg-org-select-with-descendants labelText="Owning library" i18n-labelText (ouChange)="handleOwnerChange($event)">
+          </eg-org-select-with-descendants>
+        </div>
+        <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-resource-type-combobox #rt domId="ideal-resource-type" (typeChanged)="handleResourceTypeChange($event)"></eg-resource-type-combobox>
+          </div>
         </div>
       </div>
-    </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" (onChangeAsDate)="handleDateChange($event)" [initialDate]="idealDate"></eg-date-select>
-      <eg-daterange-select *ngIf="multiday" #dateRangeLimiter (onChange)="fetchData()"></eg-daterange-select>
-    </div>
-  </div>
-  <div class="col">
-    <div class="input-group">
-      <div class="input-group-prepend">
-        <label class="input-group-text" for="ideal-resource-barcode" i18n>Resource barcode</label>
+    </ng-template>
+  </ngb-tab>
+
+  <ngb-tab id="select-resources">
+    <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" i18n-placeholder placeholder="Resource barcode" formControlName="resourceBarcode">
+          </div>
+        </div>
       </div>
-      <input type="text" id="ideal-resource-barcode" class="form-control" i18n-placeholder placeholder="Resource barcode" [(ngModel)]="resourceBarcode" (change)="useCurrentResourceBarcode()">
-    </div>
-  </div>
-  <div class="col">
-    <div class="input-group">
-      <div class="input-group-prepend">
-        <label class="input-group-text" for="ideal-resource-type" i18n>Resource type</label>
+    </ng-template>
+  </ngb-tab>
+
+  <ngb-tab id="select-dates">
+    <ng-template ngbTabTitle>
+      <span class="material-icons">calendar_today</span>
+      <ng-container i18n>Select dates - () selected</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-reservation-type" i18n>Reservation type</label>
+            </div>
+            <select class="form-control" id="ideal-reservation-type" formControlName="reservationType">
+              <option value="single" i18n>Single day reservation</option>
+              <option value="multi" i18n>Multiple day reservation</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" (onChangeAsDate)="handleDateChange($event)" [initialDate]="idealDate"></eg-date-select>
+            <eg-daterange-select *ngIf="multiday" #dateRangeLimiter (onChange)="fetchData()"></eg-daterange-select>
+          </div>
+        </div>
       </div>
-      <eg-resource-type-combobox #rt domId="ideal-resource-type" (typeChanged)="handleResourceTypeChange($event)"></eg-resource-type-combobox>
-    </div>
-  </div>
-</div>
-<hr class="mt1" />
-<button
-  class="btn btn-primary"
-  (click)="advancedCollapsed = !advancedCollapsed"
-  [attr.aria-expanded]="!advancedCollapsed"
-  aria-controls="advanced">
-  <span *ngIf="advancedCollapsed" class="material-icons">lock</span>
-  <span *ngIf="!advancedCollapsed" class="material-icons">lock_open</span>
-  <span *ngIf="advancedCollapsed" i18n>Show advanced options</span>
-  <span *ngIf="!advancedCollapsed" i18n>Hide advanced options</span>
-</button>
+    </ng-template>
+  </ngb-tab>
 
-<div id="advanced" class="row" [ngbCollapse]="advancedCollapsed">
-  <div class="card col-md-6">
-    <div class="card-header" i18n>Display options</div>
-    <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>
+  <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">
+        <li *ngFor="let attribute of attributes" 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 (onChange)="limitByAttr(attribute.id(), $event)">
+              <eg-combobox-entry *ngFor="let value of attribute.valid_values()"
+                [entryId]="value.id()" [entryLabel]="value.valid_value()">
+              </eg-combobox-entry>
+            </eg-combobox>
           </span>
-          <ngb-timepicker [(ngModel)]="startOfDay" (ngModelChange)="fetchData()" [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>
+        </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>
-          <ngb-timepicker [(ngModel)]="endOfDay" (ngModelChange)="fetchData()" [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="granularity" i18n>Granularity</label>
+        </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>
-          <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>
-  </div>
-  <div *ngIf="attributes.length" class="card col-md-6">
-    <div class="card-header" i18n>Filter by attributes</div>
-    <ul class="list-group list-group-flush">
-      <li *ngFor="let attribute of attributes" 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>
+        </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>
-          <eg-combobox (onChange)="limitByAttr(attribute.id(), $event)">
-            <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>
-  </div>
-</div>
+        </li>
+      </ul>
+    </ng-template>
+  </ngb-tab>
+
+</ngb-tabset>
+</form>
+
 
 <eg-grid *ngIf="resources.length" #scheduleGrid
   [sortable]="false"
-  (onRowActivate)="openCreateDialog([$event])"
+  (onRowActivate)="openTheDialog([$event])"
   [dataSource]="scheduleSource"
   [rowFlairIsEnabled]="true"
   [rowFlairCallback]="resourceAvailabilityIcon"
   [disablePaging]="true"
   persistKey="disabled">
-  <eg-grid-toolbar-action label="Create Reservation" i18n-label [action]="openCreateDialog"></eg-grid-toolbar-action>
+  <eg-grid-toolbar-action label="Create Reservation" i18n-label (onClick)="openTheDialog()"></eg-grid-toolbar-action>
   <eg-grid-column path="time" [index]="true" ></eg-grid-column>
   <eg-grid-column *ngFor="let resource of resources" path="{{resource.barcode()}}" [cellTemplate]="reservationsTemplate" [disableTooltip]="true"></eg-grid-column>
 </eg-grid>
 
-<eg-fm-record-editor #newDialog
-  idlClass="bresv"
-  [fieldOptions]="{usr:{customTemplate:{template:patronTemplate}},start_time:{customTemplate:{template:datetimeWithDefaults}},end_time:{customTemplate:{template:datetimeWithDefaults}},pickup_lib:{customTemplate:{template:pickupLibrary}},target_resource:{customTemplate:{template:targetResource}}}"
-  hiddenFields="id,xact_start,request_time,capture_time,pickup_time,return_time,capture_staff,xact_finish,cancel_time,booking_interval,unrecovered,request_lib,fine_interval,fine_amount,max_fine,current_resource,target_resource_type">
-</eg-fm-record-editor>
+<eg-create-reservation-dialog #createDialog
+  [targetResourceBarcode]="resourceBarcode"
+  [targetResource]="resourceId">
+</eg-create-reservation-dialog>
 
 <ng-template #reservationsTemplate let-row="row" let-col="col">
   <ng-container *ngIf="row[col.name]">
     </ul>
   </ng-container>
 </ng-template>
-<ng-template #patronTemplate let-record="record">
-<input type="hidden" value="{{record.request_lib(auth.user().ws_ou())}}">
-  <ng-container *ngIf="patronId">
-    <input *ngIf="patronId" type="text" disabled value="{{record.usr(patronId)}}" class="form-control" name="usr">
-  </ng-container>
-  <div *ngIf="!patronId" 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" [(ngModel)]="patronBarcode" (change)="findPatronByBarcode()">
-    </div>
-  </div>
-</ng-template>
-<ng-template #datetimeWithDefaults let-record="record" let-field="field">
-  <input type="hidden" value="{{record[field.name](defaultTimes[field.name].toISOString())}}">
-  <eg-datetime-select
-    [showTZ]="true"
-    [minuteStep]="minuteStep()"
-    [timezone]="pickupLibUsesDifferentTz ? pickupLibUsesDifferentTz : format.wsOrgTimezone"
-    (onChangeAsIso)="record[field.name]($event)"
-    (onChangeAsMoment)="field.validatorError = reservationValidate[field.name](field.name, $event, record)"
-    [validatorError]="field.validatorError"
-    [initialMoment]="defaultTimes[field.name]">
-  </eg-datetime-select>
-</ng-template>
-<ng-template #pickupLibrary let-record="record" let-field="field">
-  <input type="hidden" value="{{record.pickup_lib(auth.user().ws_ou())}}">
-  <eg-org-select
-    [initialOrgId]="auth.user().ws_ou()"
-    (onChange)="handlePickupLibChange($event)">
-  </eg-org-select>
-  <div *ngIf="pickupLibUsesDifferentTz" class="alert alert-primary" i18n>Pickup library uses a different timezone than your library does. Please choose times in the pickup library's timezone.</div>
-</ng-template>
-<ng-template #targetResource let-record="record">
-  <input type="hidden" value="{{record.target_resource_type(resourceTypeId)}}">
-  <ng-container *ngIf="resourceId">
-    <input type="text" disabled value="{{resourceBarcode}}" class="form-control">
-    <input type="hidden" value="{{record.target_resource(resourceId)}}">
-    <input type="hidden" value="{{record.current_resource(resourceId)}}">
-  </ng-container>
-  <ng-container *ngIf="!resourceId">
-    <eg-combobox (onChange)="handleTargetResourceChange($event.id)" 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>
-  </ng-container>
-</ng-template>
index c2fce86..015e9bc 100644 (file)
@@ -1,13 +1,13 @@
 import {Component, Input, OnInit, AfterViewInit, QueryList, ViewChildren, ViewChild} from '@angular/core';
+import {FormGroup, FormControl} from "@angular/forms";
 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
-import {forkJoin} from 'rxjs';
-import {single} from 'rxjs/operators';
+import {forkJoin, of, timer} from 'rxjs';
+import {catchError, debounceTime, mapTo, single, switchMap} from 'rxjs/operators';
 import {NgbDateStruct, NgbTimeStruct} from '@ng-bootstrap/ng-bootstrap';
 import {AuthService} from '@eg/core/auth.service';
 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
 import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
 import {DateRangeSelectComponent} from '@eg/share/daterange-select/daterange-select.component';
-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,  GridRowFlairEntry} from '@eg/share/grid/grid';
@@ -16,6 +16,7 @@ import {NetService} from '@eg/core/net.service';
 import {OrgService} from '@eg/core/org.service';
 import {PatronService} from '@eg/staff/share/patron.service';
 import {PcrudService} from '@eg/core/pcrud.service';
+import {CreateReservationDialogComponent} from './create-reservation-dialog.component';
 import {ResourceTypeComboboxComponent} from './resource-type-combobox.component';
 import {ServerStoreService} from '@eg/core/server-store.service';
 import {ToastService} from '@eg/share/toast/toast.service';
@@ -30,7 +31,8 @@ import * as Moment from 'moment-timezone';
 
 export class CreateReservationComponent implements OnInit, AfterViewInit {
 
-    advancedCollapsed = true;
+    criteria: FormGroup;
+
     attributes: IdlObject[] = [];
     selectedAttributes: number[] = [];
     multiday = false;
@@ -60,12 +62,10 @@ export class CreateReservationComponent implements OnInit, AfterViewInit {
 
     minuteStep: () => number;
 
-    openCreateDialog: (rows: IdlObject[]) => void;
     openTheDialog: (rows: IdlObject[]) => any;
 
     resources: IdlObject[] = [];
     limitByAttr: (attributeId: number, $event: ComboboxEntry) => void;
-    useCurrentResourceBarcode: () => void;
     findPatronByBarcode: () => void;
 
     setGranularity: () => void;
@@ -78,8 +78,8 @@ export class CreateReservationComponent implements OnInit, AfterViewInit {
     @ViewChildren('dateLimiter') dateLimiters: QueryList<DateSelectComponent>;
     @ViewChildren('dateRangeLimiter') dateRangeLimiters: QueryList<DateRangeSelectComponent>;
     @ViewChildren('scheduleGrid') scheduleGrids: QueryList<GridComponent>;
-    @ViewChild('newDialog') newDialog: FmRecordEditorComponent;
     @ViewChild('rt') rt: ResourceTypeComboboxComponent;
+    @ViewChild('createDialog') createDialog: CreateReservationDialogComponent;
 
     idealDate = new Date();
 
@@ -155,6 +155,37 @@ export class CreateReservationComponent implements OnInit, AfterViewInit {
             }
         });
 
+        this.criteria = new FormGroup({
+            'resourceBarcode': new FormControl(this.resourceBarcode ? this.resourceBarcode : '',
+                [], (rb) => 
+                    timer(800).pipe(switchMap(() =>
+                        this.pcrud.search('brsrc',
+                            {'barcode' : rb.value},
+                            {'limit': 1})),
+                        single(),
+                        mapTo(null),
+                        catchError(() => of({ resourceBarcode: 'No resource found with that barcode' }))
+                    )),
+            'startOfDay': new FormControl(this.startOfDay),
+            'endOfDay': new FormControl(this.endOfDay),
+            'reservationType': new FormControl(this.multiday ? 'multi' : 'single'),
+        });
+
+       this.criteria.get('resourceBarcode').valueChanges
+        .pipe(debounceTime(1000))
+        .subscribe((barcode) => {
+            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.criteria.get('reservationType').valueChanges.subscribe((val) => 
+            this.store.setItem('eg.booking.create.multiday', ('multi' === val)));
+
+       this.criteria.valueChanges.subscribe(() => { this.fetchData(); });
+
         this.limitByAttr = (attributeId: number, $event: ComboboxEntry) => {
             this.selectedAttributes[attributeId] = $event.id;
             this.fetchData();
@@ -196,18 +227,6 @@ export class CreateReservationComponent implements OnInit, AfterViewInit {
             this.fetchData();
         };
 
-        this.handleMultiDayReservation = () => {
-            this.multiday = true;
-            this.store.setItem('eg.booking.create.multiday', true);
-            this.fetchData();
-        };
-
-        this.handleSingleDayReservation = () => {
-            this.multiday = false;
-            this.store.setItem('eg.booking.create.multiday', false);
-            this.handleDateChange(new Date());
-        };
-
         this.changeGranularity = ($event) => {
             this.granularity = $event.id;
             this.store.setItem('eg.booking.create.granularity', $event.id)
@@ -215,7 +234,6 @@ export class CreateReservationComponent implements OnInit, AfterViewInit {
         };
 
         this.handlePickupLibChange = ($event) => {
-            this.newDialog.record.pickup_lib($event);
             this.org.settings('lib.timezone', $event.id()).then((tz) => {
                 if (tz['lib.timezone'] && (this.format.wsOrgTimezone !== tz['lib.timezone'])) {
                     this.pickupLibUsesDifferentTz = tz['lib.timezone'];
@@ -225,28 +243,6 @@ export class CreateReservationComponent implements OnInit, AfterViewInit {
             });
         };
 
-        this.handleTargetResourceChange = ($event) => {
-            if ('any' !== $event) {
-                this.newDialog.record.current_resource($event);
-                this.newDialog.record.target_resource($event);
-            }
-        };
-
-        this.useCurrentResourceBarcode = () => {
-            if (this.resourceBarcode) {
-                this.router.navigate(['/staff', 'booking', 'create_reservation', 'for_resource', this.resourceBarcode]);
-            }
-        };
-
-        this.findPatronByBarcode = () => {
-            if (this.patronBarcode) {
-                this.patron.bcSearch(this.patronBarcode).pipe(single()).subscribe(
-                    resp => { this.newDialog.record.usr(resp[0].id); },
-                    err => { this.toast.danger('No patron found with this barcode'); },
-                );
-            }
-        };
-
         this.minuteStep = () => {
             return (this.granularity < 60) ? this.granularity : 30;
         };
@@ -258,7 +254,7 @@ export class CreateReservationComponent implements OnInit, AfterViewInit {
         this.fetchData();
 
         this.openTheDialog = (rows: IdlObject[]) => {
-            return this.newDialog.open({size: 'lg'}).subscribe(
+            return this.createDialog.open({size: 'lg'}).subscribe(
                 response => {
                     this.toast.success('Reservation successfully created'); // TODO: needs i18n, pluralization
                     this.fetchData();
@@ -267,63 +263,6 @@ export class CreateReservationComponent implements OnInit, AfterViewInit {
             );
         };
 
-        this.openCreateDialog = (rows: IdlObject[]) => {
-            if (rows.length) {
-                if (this.multiday) {
-                    this.defaultTimes['start_time'] = this.format.momentizeDateString(rows[0]['time'], this.format.wsOrgTimezone);
-                    this.defaultTimes['end_time'] = this.format.momentizeDateString(
-                        rows[rows.length - 1]['time'], this.format.wsOrgTimezone).clone()
-                        .add(this.granularity, 'minutes');
-                } else {
-                    this.defaultTimes['start_time'] = Moment.tz('' +
-                        this.idealDate.getFullYear() + '-' +
-                        (this.idealDate.getMonth() + 1) + '-' +
-                        (this.idealDate.getDate()) + ' ' + rows[0]['time'],
-                        'YYYY-MM-DD LT', this.format.wsOrgTimezone);
-                    this.defaultTimes['end_time'] = Moment.tz('' +
-                        this.idealDate.getFullYear() + '-' +
-                        (this.idealDate.getMonth() + 1) + '-' +
-                        (this.idealDate.getDate()) + ' ' + rows[rows.length - 1]['time'],
-                        'YYYY-MM-DD LT', this.format.wsOrgTimezone).clone().add(this.granularity, 'minutes');
-                }
-            } else {
-                if (this.multiday) { this.defaultTimes['end_time'] = this.defaultTimes['start_time'].clone().add(1, 'days'); }
-            }
-            if (this.resourceId && !this.resourceTypeId) {
-                this.pcrud.search('brsrc', {id: this.resourceId}, {
-                    flesh: 1,
-                    limit: 1,
-                    flesh_fields: {'brsrc': ['type']}
-                }).subscribe( r => {
-                    this.transferable = r.type().transferable();
-                    this.resourceTypeId = r.type().id();
-                    this.resourceOwner = r.owner();
-                    this.openTheDialog(rows);
-                });
-            } else if (this.resourceTypeId) {
-                this.pcrud.search('brt', {id: this.resourceTypeId}, {
-                }).subscribe( t => {
-                    this.transferable = t.transferable();
-                    this.openTheDialog(rows).then(newId => {
-                        if (this.selectedAttributes.length) {
-                            const creates$ = [];
-                            this.selectedAttributes.forEach(attrValue => {
-                                if (attrValue) {
-                                    const bravm = this.idl.create('bravm');
-                                    bravm.attr_value(attrValue);
-                                    bravm.reservation(newId);
-                                    creates$.push(this.pcrud.create(bravm));
-                                }
-                            });
-                            forkJoin(...creates$).subscribe(() => {
-                            this.net.request('open-ils.storage', 'open-ils.storage.booking.reservation.resource_targeter', [newId]); });
-                        } else {
-                            this.net.request('open-ils.storage', 'open-ils.storage.booking.reservation.resource_targeter', [newId]);
-                        }
-                    });
-                });
-            }
-        };
     }
     handleResourceTypeChange($event: ComboboxEntry) {
         this.resourceBarcode = null;
@@ -349,12 +288,13 @@ export class CreateReservationComponent implements OnInit, AfterViewInit {
     fetchData () {
         this.setGranularity();
         this.resources = [];
-        const where = {'owner': this.owningLibraries};
+        let where = {};
 
         if (this.resourceId) {
             where['id'] = this.resourceId;
         } else if (this.resourceTypeId) {
             where['type'] = this.resourceTypeId;
+            where['owner'] = this.owningLibraries;
         } else {
             return;
         }
@@ -388,15 +328,15 @@ export class CreateReservationComponent implements OnInit, AfterViewInit {
                             dl.current.year,
                             dl.current.month - 1,
                             dl.current.day,
-                            this.startOfDay.hour,
-                            this.startOfDay.minute],
+                            this.userStartOfDay.hour,
+                            this.userStartOfDay.minute],
                             this.format.wsOrgTimezone);
                         endTime = Moment.tz([
                             dl.current.year,
                             dl.current.month - 1,
                             dl.current.day,
-                            this.endOfDay.hour,
-                            this.endOfDay.minute],
+                            this.userEndOfDay.hour,
+                            this.userEndOfDay.minute],
                             this.format.wsOrgTimezone);
                     });
                 }
@@ -438,6 +378,12 @@ export class CreateReservationComponent implements OnInit, AfterViewInit {
                 });
             });
     }
+    get userStartOfDay() {
+        return this.criteria.get('startOfDay').value;
+    }
+    get userEndOfDay() {
+        return this.criteria.get('startOfDay').value;
+    }
 
 }
 
index 9926147..c77b28c 100644 (file)
@@ -21,7 +21,7 @@
                 <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" i18n-placeholder placeholder="Patron barcode" [(ngModel)]="patronBarcode" (change)="filterByCurrentPatronBarcode()">
+                <input type="text" id="patron-barcode-value" class="form-control" i18n-placeholder placeholder="Patron barcode" egValidPatronBarcode [(ngModel)]="patronBarcode" (change)="filterByCurrentPatronBarcode()">
                 <div class="input-group-button">
                   <button *ngIf="patronBarcode" class="btn btn-warning" (click)="removeFilters()" i18n><span class="material-icons">delete</span> Remove filter</button>
                 </div>
index e4f1fad..6d5a763 100644 (file)
@@ -28,6 +28,8 @@ import {EgHelpPopoverComponent} from '@eg/share/eg-help-popover/eg-help-popover.
 import {ReactiveFormsModule} from '@angular/forms';
 import {DatetimeValidatorDirective} from '@eg/share/validators/datetime_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.
  */
@@ -55,6 +57,7 @@ import {DatetimeValidatorDirective} from '@eg/share/validators/datetime_validato
     AdminPageComponent,
     EgHelpPopoverComponent,
     DatetimeValidatorDirective,
+    PatronBarcodeValidatorDirective
   ],
   imports: [
     EgCommonModule,
@@ -85,6 +88,7 @@ import {DatetimeValidatorDirective} from '@eg/share/validators/datetime_validato
     AdminPageComponent,
     EgHelpPopoverComponent,
     DatetimeValidatorDirective,
+    PatronBarcodeValidatorDirective
   ]
 })