--- /dev/null
+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();
+ });
+ });
+});
+
--- /dev/null
+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);
+ }
+}
+
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';
providers: [PatronService],
declarations: [
CreateReservationComponent,
+ CreateReservationDialogComponent,
ManageReservationsComponent,
NoTimezoneSetComponent,
OrgSelectWithDescendantsComponent,
--- /dev/null
+<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">
+ </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>
--- /dev/null
+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();
+ }
+ );
+ }
+
+ }
+}
+
</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>
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';
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';
export class CreateReservationComponent implements OnInit, AfterViewInit {
- advancedCollapsed = true;
+ criteria: FormGroup;
+
attributes: IdlObject[] = [];
selectedAttributes: number[] = [];
multiday = false;
minuteStep: () => number;
- openCreateDialog: (rows: IdlObject[]) => void;
openTheDialog: (rows: IdlObject[]) => any;
resources: IdlObject[] = [];
limitByAttr: (attributeId: number, $event: ComboboxEntry) => void;
- useCurrentResourceBarcode: () => void;
findPatronByBarcode: () => void;
setGranularity: () => void;
@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();
}
});
+ 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();
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)
};
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'];
});
};
- 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;
};
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();
);
};
- 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;
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;
}
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);
});
}
});
});
}
+ get userStartOfDay() {
+ return this.criteria.get('startOfDay').value;
+ }
+ get userEndOfDay() {
+ return this.criteria.get('startOfDay').value;
+ }
}
<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>
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.
*/
AdminPageComponent,
EgHelpPopoverComponent,
DatetimeValidatorDirective,
+ PatronBarcodeValidatorDirective
],
imports: [
EgCommonModule,
AdminPageComponent,
EgHelpPopoverComponent,
DatetimeValidatorDirective,
+ PatronBarcodeValidatorDirective
]
})