<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"/>
<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"/>
<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>
"bootstrap-css-only": "^4.2.1",
"core-js": "^2.6.3",
"file-saver": "^2.0.0",
+ "moment": "2.24.0",
+ "moment-timezone": "0.5.23",
"ngx-cookie": "^4.1.2",
"rxjs": "^6.4.0",
"zone.js": "^0.8.29"
import {DatePipe, CurrencyPipe} from '@angular/common';
import {IdlService, IdlObject} from '@eg/core/idl.service';
import {OrgService} from '@eg/core/org.service';
+import * as Moment from 'moment-timezone';
/**
* Format IDL vield values for display.
datatype?: string;
orgField?: string; // 'shortname' || 'name'
datePlusTime?: boolean;
+ timezoneContextOrg?: number;
}
@Injectable({providedIn: 'root'})
return org ? org[orgField]() : '';
case 'timestamp':
- const date = new Date(value);
- let fmt = this.dateFormat || 'shortDate';
- if (params.datePlusTime) {
- fmt = this.dateTimeFormat || 'short';
+ const date = Moment(value).tz(this.wsOrgTimezone);
+ if (date) {
+ let fmt = this.dateFormat || 'shortDate';
+ if (params.datePlusTime) {
+ fmt = this.dateTimeFormat || 'short';
+ }
+ return this.datePipe.transform(date.toISOString(true), fmt, date.format('ZZ'));
}
- return this.datePipe.transform(date, fmt);
+ return value;
case 'money':
return this.currencyPipe.transform(value);
<div class="input-group">
- <input
- class="form-control"
+ <input
+ class="form-control"
ngbDatepicker
#datePicker="ngbDatepicker"
[attr.id]="domId.length ? domId : null"
<div class="input-group-append">
<button class="btn btn-outline-secondary" [disabled]="_disabled"
(click)="datePicker.toggle()" type="button">
- <span title="Select Date" i18n-title
+ <span title="Select Date" i18n-title
class="material-icons mat-icon-in-button">calendar_today</span>
</button>
</div>
--- /dev/null
+<ngb-datepicker #dp (select)="onDateSelection($event)" [displayMonths]="2" [dayTemplate]="t" outsideDays="hidden">
+</ngb-datepicker>
+
+<ng-template #t let-date let-focused="focused">
+ <span class="custom-day"
+ [class.focused]="focused"
+ [class.range]="isRange(date)"
+ [class.faded]="isHovered(date) || isInside(date)"
+ (mouseenter)="hoveredDate = date"
+ (mouseleave)="hoveredDate = null">
+ {{ date.day }}
+ </span>
+</ng-template>
+
+<hr>
+
+<pre>From: {{ fromDate | json }} </pre>
+<pre>To: {{ toDate | json }} </pre>
--- /dev/null
+import {Component} from '@angular/core';
+import {NgbDate, NgbCalendar} from '@ng-bootstrap/ng-bootstrap';
+
+@Component({
+ selector: 'eg-daterange-select',
+ templateUrl: './daterange-select.component.html',
+ styles: [`
+ .custom-day {
+ text-align: center;
+ padding: 0.185rem 0.25rem;
+ display: inline-block;
+ height: 2rem;
+ width: 2rem;
+ }
+ .custom-day.focused {
+ background-color: #e6e6e6;
+ }
+ .custom-day.range, .custom-day:hover {
+ background-color: rgb(2, 117, 216);
+ color: white;
+ }
+ .custom-day.faded {
+ background-color: rgba(2, 117, 216, 0.5);
+ }
+ `]
+})
+export class DateRangeSelectComponent {
+
+ hoveredDate: NgbDate;
+
+ fromDate: NgbDate;
+ toDate: NgbDate;
+
+ constructor(calendar: NgbCalendar) {
+ this.fromDate = calendar.getToday();
+ this.toDate = calendar.getNext(calendar.getToday(), 'd', 10);
+ }
+
+ onDateSelection(date: NgbDate) {
+ if (!this.fromDate && !this.toDate) {
+ this.fromDate = date;
+ } else if (this.fromDate && !this.toDate && date.after(this.fromDate)) {
+ this.toDate = date;
+ } else {
+ this.toDate = null;
+ this.fromDate = date;
+ }
+ }
+
+ isHovered(date: NgbDate) {
+ return this.fromDate && !this.toDate && this.hoveredDate && date.after(this.fromDate) && date.before(this.hoveredDate);
+ }
+
+ isInside(date: NgbDate) {
+ return date.after(this.fromDate) && date.before(this.toDate);
+ }
+
+ isRange(date: NgbDate) {
+ return date.equals(this.fromDate) || date.equals(this.toDate) || this.isInside(date) || this.isHovered(date);
+ }
+}
--- /dev/null
+<!-- TODO:
+* Have this accept ISO input (i.e. from the database)
+* Have this accept @Input params of timezone (default to OU), disable
+* turn this into a popover that pops over the input: https://ng-bootstrap.github.io/#/components/popover/examples
+* have this input and output the five and noble stuff in the correct format (currently you can't just type ssomething into the box and have it show up on the calendar, but it does show up in the model - cool)
+
+-->
+<div class="input-group">
+ <input type="datetime"
+ [attr.id]="domId.length ? domId : null"
+ name="{{fieldName}}"
+ class="form-control"
+ [placeholder]="initialIso"
+ [(ngModel)]="dateTime"
+ (blur)="blurred($event)"
+ #dtPicker="ngbPopover"
+ [ngbPopover]="dt"
+ placement="bottom"
+ [autoClose]="'outside'"
+ popoverTitle="Select date and time"
+ i18n-popoverTitle
+ [required]="required">
+ <div class="input-group-btn">
+ <button class="btn btn-primary" (click)="dtPicker.toggle()">
+ <span title="Select Date" i18n-title
+ class="material-icons mat-icon-in-button">calendar_today</span>
+ </button>
+ </div>
+</div>
+
+<ng-template #dt>
+ <ngb-datepicker
+ [(ngModel)]="dateModel"
+ (ngModelChange)="modelChanged($event)"
+ [footerTemplate]="time">
+ </ngb-datepicker>
+</ng-template>
+<ng-template #time>
+ <ngb-timepicker name="time"
+ [(ngModel)]="timeModel" [meridian]="true"
+ (ngModelChange)="modelChanged()"
+ [spinners]="true"
+ [hourStep]="1"
+ [minuteStep]="minuteStep || 15" >
+ </ngb-timepicker>
+ <span *ngIf="showTZ" class="badge badge-info">America/Los_Angeles</span>
+ <button i18n class="btn btn-success" (click)="dtPicker.close()">Choose time</button>
+</ng-template>
+
--- /dev/null
+import { Component, Input, Output, EventEmitter, ViewChild, OnInit } from '@angular/core';
+import { ControlValueAccessor } from '@angular/forms';
+
+import * as Moment from 'moment-timezone';
+
+import { NgbTimeStruct, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
+
+@Component({
+ selector: 'eg-datetime-select',
+ templateUrl: './datetime-select.component.html'
+})
+export class DateTimeSelectComponent implements OnInit {
+ @Input() formModel: any; // External model bound back to parent
+ @Input() domId = '';
+ @Input() fieldName: string;
+ @Input() required: boolean;
+ @Input() minuteStep: number;
+ @Input() showTZ = true;
+
+ @Input() initialIso: string;
+
+ @Output() formModelChange = new EventEmitter();
+
+ dateTime: any; // Used internally on internal input
+ timeModel: NgbTimeStruct;
+ dateModel: NgbDateStruct;
+
+ constructor() {
+ }
+
+ ngOnInit() {
+ const start = this.formModel ? Moment(this.formModel) : Moment();
+ this.setDefaultDate(start);
+ this.setDefaultTime(start);
+
+ if (this.formModel) {
+ this.modelChanged(null);
+ }
+ }
+
+ setDefaultDate(start: any) {
+ this.dateModel = { year: start.year(), month: start.month() + 1, day: start.date() };
+ }
+
+ setDefaultTime(start: any) {
+ const remainder = 5 - start.minute() % 5,
+ final = Moment(start).add(remainder, 'minutes');
+
+ // Seed time model with current, rounding up to nearest 5 minutes (does roll hour over if needed)
+ this.timeModel = { hour: final.hour(), minute: final.minute(), second: 0 };
+ }
+
+
+ blurred(event) {
+ this.modelChanged(event);
+ }
+
+ modelChanged(event) {
+ let newDate: any;
+
+ if (event) {
+ newDate = Moment(new Date(this.dateTime));
+ } else {
+ newDate = new Date(this.dateModel.year, this.dateModel.month, this.dateModel.day,
+ this.timeModel.hour, this.timeModel.minute, this.timeModel.second);
+ }
+
+ if (newDate && !isNaN(newDate)) {
+ console.log('newDate');
+ // Set component view value
+ this.dateTime = Moment(newDate).format('MM/D/YYYY h:mm A');
+ // Update form passed in view value
+ this.formModelChange.emit(Moment(newDate));
+ }
+ }
+
+ isDisabled(date: NgbDateStruct, current: { month: number }) {
+ return date.month !== current.month;
+ }
+
+}
+
(ngModelChange)="record[field.name]($event)"/>
</ng-container>
+ <ng-container *ngSwitchCase="'readonly-au'">
+ <a href="/eg/staff/circ/patron/{{field.linkedValues[0].id}}/checkout" target="_blank">{{field.linkedValues[0]['name']}}
+ <span class="material-icons" i18n-title title="Open user record in new tab">open_in_new</span></a>
+ </ng-container>
+
<ng-container *ngSwitchCase="'list'">
<eg-combobox
id="{{idPrefix}}-{{field.name}}" name="{{field.name}}"
return 'readonly-money';
}
+ if ((field.datatype === 'link' || field.linkedValues) && field.class === 'au') {
+ return 'readonly-au';
+ }
+
if (field.datatype === 'link' || field.linkedValues) {
return 'readonly-list';
}
// 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>;
col.isMultiSortable = this.multiSortable;
col.datatype = this.datatype;
col.datePlusTime = this.datePlusTime;
+ col.timezoneContextOrg = this.timezoneContextOrg;
col.isAuto = false;
this.grid.context.columnSet.add(col);
}
<span class="ml-2" i18n>Manage Column Widths</span>
</a>
<a class="dropdown-item label-with-material-icon"
+ [class.disabled]="disableSaveSettings"
(click)="saveGridConfig()">
<span class="material-icons">save</span>
<span class="ml-2" i18n>Save Grid Settings</span>
@Input() gridContext: GridContext;
@Input() colWidthConfig: GridColumnWidthComponent;
@Input() gridPrinter: GridPrintComponent;
+ @Input() disableSaveSettings = false;
csvExportInProgress: boolean;
csvExportUrl: SafeUrl;
<eg-grid-toolbar
[gridContext]="context"
[gridPrinter]="gridPrinter"
- [colWidthConfig]="colWidthConfig">
+ [colWidthConfig]="colWidthConfig"
+ [disableSaveSettings]="disableSaveSettings">
</eg-grid-toolbar>
<eg-grid-header [context]="context"></eg-grid-header>
// field on the "aout" class.
@Input() showLinkSelectors: boolean;
+ // If true, users won't be able to save any changes they make
+ // to grid settings
+ @Input() disableSaveSettings = false;
+
context: GridContext;
// These events are emitted from our grid-body component.
idlFieldDef: any;
datatype: string;
datePlusTime: boolean;
+ timezoneContextOrg: number;
cellTemplate: TemplateRef<any>;
cellContext: any;
isIndex: boolean;
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)
});
}
--- /dev/null
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {BookingRoutingModule} from './routing.module';
+import {CreateReservationComponent} from './create-reservation.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 {ResourceTypeComboboxComponent} from './resource-type-combobox.component';
+import {ReturnComponent} from './return.component';
+import {NoTimezoneSetComponent} from './no-timezone-set.component';
+import {PatronService} from '@eg/staff/share/patron.service';
+
+
+@NgModule({
+ imports: [
+ StaffCommonModule,
+ BookingRoutingModule,
+ ],
+ providers: [PatronService],
+ declarations: [
+ CreateReservationComponent,
+ ManageReservationsComponent,
+ NoTimezoneSetComponent,
+ PickupComponent,
+ PullListComponent,
+ ReservationsGridComponent,
+ ResourceTypeComboboxComponent,
+ ReturnComponent]
+})
+export class BookingModule { }
+
--- /dev/null
+<h2>Resources</h2>
+{{resources | json}}
+<h2>Grid</h2>
+{{scheduleSource.data | json}}
+<eg-staff-banner bannerText="Create Reservation" i18n-bannerText>
+</eg-staff-banner>
+<eg-title i18n-prefix i18n-suffix prefix="Booking" suffix="Create Reservation"></eg-title>
+
+<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>
+ <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)="multiday = false" class="btn btn-outline-primary" ngbDropdownItem><span class="material-icons">event</span><span i18n>Single day reservation</span></button>
+ <button (click)="multiday = true" class="btn btn-outline-primary" ngbDropdownItem><span class="material-icons">date_range</span><span i18n>Multiple day reservation</span></button>
+ </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"></eg-date-select>
+ <eg-daterange-select *ngIf="multiday" #dateRangeLimiter></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>
+ </div>
+ <input type="text" id="ideal-resource-barcode" class="form-control" i18n-placeholder placeholder="Resource barcode">
+ </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>
+ </div>
+ <eg-resource-type-combobox domId="ideal-resource-type" (onChange)="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>
+
+<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>
+ </span>
+ <ngb-timepicker [(ngModel)]="startTime"></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 [(ngModel)]="endTime"></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>
+ </span>
+ <eg-combobox (onChange)="granularity = $event.entryId">
+ <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>
+ </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>
+
+<eg-grid *ngIf="resources.length" #scheduleGrid
+ [sortable]="false"
+ (onRowActivate)="showNewDialog($event)"
+ [dataSource]="scheduleSource"
+ [rowFlairIsEnabled]="true"
+ [rowFlairCallback]="resourceAvailabilityIcon"
+ disableSaveSettings="true"
+ [cellClassCallback]="isBooked">
+ <eg-grid-column path="time" [index]="true" ></eg-grid-column>
+ <eg-grid-column *ngFor="let resource of resources" path="{{resource.barcode()}}"></eg-grid-column>
+</eg-grid>
+
+<eg-fm-record-editor #newDialog
+ idlClass="bresv"
+ hiddenFields="id,xact_start,request_time,capture_time,pickup_time,return_time,capture_staff,xact_finish,cancel_time,booking_interval,target_resource,unrecovered,request_library,fine_interval,fine_amount,max_fine">
+</eg-fm-record-editor>
+
--- /dev/null
+import { Component, Input, OnInit, ViewChild } from '@angular/core';
+import { NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource, GridRowFlairEntry} from '@eg/share/grid/grid';
+import {IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+
+
+@Component({
+ templateUrl: './create-reservation.component.html'
+})
+
+export class CreateReservationComponent implements OnInit {
+
+ advancedCollapsed = true;
+ attributes: IdlObject[] = [];
+ multiday = false;
+ isBooked: (col: any, row: any) => string;
+ resourceAvailabilityIcon: (row: any) => GridRowFlairEntry;
+ fetchData: (limiter: 'resource' | 'type', id: number) => void;
+
+ startTime: NgbTimeStruct = {hour: 9, minute: 0, second: 0};
+ endTime: NgbTimeStruct = {hour: 17, minute: 0, second: 0};
+ granularity: 15 | 30 | 60 = 30;
+
+ scheduleSource: GridDataSource = new GridDataSource();
+
+ handleResourceTypeChange: ($event: ComboboxEntry) => void;
+
+ resources: IdlObject[] = [];
+ limitByAttr: (attributeId: number, $event: ComboboxEntry) => void;
+
+ @ViewChild('dateLimiter') dateLimiter: DateSelectComponent;
+ @ViewChild('newDialog') newDialog: FmRecordEditorComponent;
+ @ViewChild('scheduleGrid') scheduleGrid: GridComponent;
+
+ constructor(
+ private pcrud: PcrudService,
+ private toast: ToastService,
+ ) {
+ }
+
+
+ ngOnInit() {
+ this.dateLimiter.initialDate = new Date();
+
+ this.fetchData = (limiter: 'resource' | 'type', id: number) => {
+ this.resources = [];
+ let where = {};
+ if ('type' === limiter) {
+ where = {type: id};
+ } else if ('resource' === limiter) {
+ where = {id: id};
+ }
+ this.pcrud.search('brsrc', where, {
+ order_by: 'barcode ASC',
+ flesh: 1,
+ flesh_fields: {'brsrc': ['curr_rsrcs']},
+ select: {'curr_rsrcs': {'end_time': {'<' : '2019-04-01'}}}
+ }).subscribe(
+ r => { this.resources.push(r); });
+
+ this.scheduleSource.data = [];
+ let current_time = this.startTime;
+ while (this._firstTimeIsSmaller(current_time, this.endTime)) {
+ this.scheduleSource.data.push({
+ 'time': current_time.hour + ':' + current_time.minute,
+ 'ROOM1231': 'Professor Pickle'
+ });
+ if ((current_time.minute + this.granularity) >= 60) {
+ current_time.hour = current_time.hour + 1;
+ }
+ current_time.minute = (current_time.minute + this.granularity) % 60;
+ }
+ }
+
+ this.isBooked = (row: any, col: any) => {
+ if ((col.name !== 'time') && (row[col.name])) {
+ return 'bg-warning';
+ }
+ };
+ this.resourceAvailabilityIcon = (row: any) => {
+ let icon = {icon: 'event_busy', title: 'All resources are reserved at this time'};
+ let busy_columns = 0;
+ for (const key in row) {
+ if (row[key]) { busy_columns = busy_columns + 1; }
+ }
+ if (busy_columns <= this.resources.length) { // equal or less than, since it counts the time column
+ icon = {icon: 'event_available', title: 'Resources are available at this time'};
+ }
+ return icon;
+ };
+
+ this.limitByAttr = (attributeId: number, $event: ComboboxEntry) => {
+ console.log('LIMIT');
+ console.log('id: ' + attributeId);
+ console.log('event: ' + JSON.stringify($event));
+ };
+
+ this.handleResourceTypeChange = ($event: ComboboxEntry) => {
+ // TODO: unset resource barcode
+ this.attributes = [];
+ if ($event.id) {
+ this.pcrud.search('bra', {resource_type : $event.id}, {
+ order_by: 'name ASC',
+ flesh: 1,
+ flesh_fields: {'bra' : ['valid_values']}
+ }).subscribe(
+ a => { this.attributes.push(a); });
+ this.fetchData('type', $event.id);
+ }
+ };
+
+ }
+ showNewDialog(idlThing: IdlObject) {
+ return this.newDialog.open({size: 'lg'}).then(
+ ok => {
+ this.toast.success('Reservation successfully created'); // TODO: needs i18n, pluralization
+ this.scheduleGrid.reload();
+ },
+ err => {}
+ );
+ }
+ private _firstTimeIsSmaller(first: NgbTimeStruct, second: NgbTimeStruct) {
+ if (first.hour < second.hour) { return true; }
+ if ((first.hour === second.hour) && (first.minute < second.minute)) { return true; }
+ return false;
+ }
+}
+
--- /dev/null
+<eg-staff-banner bannerText="Manage Reservations" i18n-bannerText>
+</eg-staff-banner>
+<eg-title i18n-prefix i18n-suffix prefix="Booking" suffix="Manage Reservations"></eg-title>
+
+<div class="card-body row">
+ <div class="col-sm-3">
+ <div class="input-group">
+ <div class="input-group-prepend">
+ <label for="pickup-library" i18n class="input-group-text">Pickup library</label>
+ </div>
+ <eg-org-select domId="pickup-library">
+ </eg-org-select>
+ </div>
+ </div>
+ <div class="col-sm-3">
+ <div class="form-check">
+ <input type="checkbox" class="form-check-input" id="include-ancestors">
+ <label class="form-check-label" for="include-ancestors" i18n>+ Ancestors</label>
+ </div>
+ <div class="form-check">
+ <input type="checkbox" class="form-check-input" id="include-descendants">
+ <label class="form-check-label" for="include-descendants" i18n>+ Descendants</label>
+ </div>
+ </div>
+ <div class="col-sm-6">
+ <div class="card">
+ <h2 class="card-header" i18n>Filter reservations</h2>
+ <ngb-tabset #filters [activeId]="selectedFilter" (tabChange)="setStickyFilter()" 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" i18n-placeholder placeholder="Patron barcode" [(ngModel)]="patronBarcode" (change)="filterByCurrentPatronBarcode()">
+ <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-tab id="resource">
+ <ng-template ngbTabTitle>
+ <span class="material-icons" *ngIf="resourceId">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" i18n-placeholder placeholder="Resource barcode" [(ngModel)]="resourceBarcode" (change)="filterByCurrentResourceBarcode()">
+ <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-tab id="type">
+ <ng-template ngbTabTitle>
+ <span class="material-icons" *ngIf="resourceTypeId">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-resource-type-combobox domId="resource-type-value" (onChange)="filterByResourceType($event)" startId="resourceTypeId"></eg-resource-type-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>
+</div>
+<eg-reservations-grid #reservationsGrid [patron]="patronId" [resource]="resourceId" [resourceType]="resourceTypeId" persistSuffix="manage"></eg-reservations-grid>
+
--- /dev/null
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {PatronService} from '@eg/staff/share/patron.service';
+import {ReservationsGridComponent} from './reservations-grid.component';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+@Component({
+ selector: 'eg-manage-reservations',
+ templateUrl: './manage-reservations.component.html',
+})
+export class ManageReservationsComponent implements OnInit {
+
+ patronBarcode: string;
+ patronId: number;
+ resourceBarcode: string;
+ resourceId: number;
+ resourceTypeId: number;
+ selectedFilter: 'patron' | 'resource' | 'type' = 'patron';
+
+ @ViewChild('reservationsGrid') reservationsGrid: ReservationsGridComponent;
+
+ filterByCurrentPatronBarcode: () => void;
+ filterByCurrentResourceBarcode: () => void;
+ filterByResourceType: (selected: ComboboxEntry) => void;
+ removeFilters: () => void;
+ chooseAppropriateFilter: () => void;
+ setStickyFilter: () => void;
+
+ constructor(
+ private route: ActivatedRoute,
+ private router: Router,
+ private pcrud: PcrudService,
+ private patron: PatronService,
+ private store: ServerStoreService
+ ) {
+ // This is in the constructor, because we need it first thing in ngOnInit
+ this.chooseAppropriateFilter = () => {
+ if (this.resourceBarcode) {
+ this.selectedFilter = 'resource';
+ } else if (this.resourceTypeId) {
+ this.selectedFilter = 'type';
+ } else if (!(this.patronId)) {
+ this.store.getItem('eg.booking.manage.filter').then(filter => {
+ if (filter) { this.selectedFilter = filter; }
+ });
+ }
+ };
+
+ }
+
+ ngOnInit() {
+ this.route.paramMap.subscribe((params: ParamMap) => {
+ this.patronId = +params.get('patron_id');
+ this.resourceBarcode = params.get('resource_barcode');
+ this.resourceTypeId = +params.get('resource_type_id');
+ this.chooseAppropriateFilter();
+ });
+
+ if (this.patronId) {
+ this.pcrud.search('au', {
+ 'id': this.patronId,
+ }, {
+ limit: 1,
+ flesh: 1,
+ flesh_fields: {'au': ['card']}
+ }).subscribe(
+ (resp) => { this.patronBarcode = resp.card().barcode(); },
+ (err) => { console.log(err); }
+ );
+ }
+
+ if (this.resourceBarcode) {
+ this.pcrud.search('brsrc',
+ {'barcode' : this.resourceBarcode}, {'limit': 1, 'select': ['id']})
+ .subscribe((res) => {
+ this.resourceId = res.id();
+ this.reservationsGrid.reloadGrid();
+ });
+ }
+
+ this.setStickyFilter = () => {
+ this.store.setItem('eg.booking.manage.filter', this.selectedFilter);
+ };
+
+ this.removeFilters = () => {
+ this.router.navigate(['/staff', 'booking', 'manage_reservations']);
+ };
+
+ this.filterByCurrentPatronBarcode = () => {
+ if (this.patronBarcode) {
+ this.patron.bcSearch(this.patronBarcode).subscribe(
+ (response) => {
+ this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_patron', response.id]);
+ });
+ } else {
+ this.removeFilters();
+ }
+ };
+
+ this.filterByCurrentResourceBarcode = () => {
+ if (this.resourceBarcode) {
+ this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_resource', this.resourceBarcode]);
+ } else {
+ this.removeFilters();
+ }
+ };
+
+ this.filterByResourceType = (selected: ComboboxEntry) => {
+ if (selected.id) {
+ this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_resource_type', selected.id]);
+ }
+ };
+
+ }
+}
+
--- /dev/null
+<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>
--- /dev/null
+import {Component, Input, ViewChild, TemplateRef} from '@angular/core';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+
+@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');
+ }
+}
--- /dev/null
+<eg-staff-banner bannerText="Booking Pickup" i18n-bannerText>
+</eg-staff-banner>
+<eg-title i18n-prefix i18n-suffix prefix="Booking" suffix="Pickup"></eg-title>
+
+<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" [(ngModel)]="patronBarcode" (change)="retrievePatron()">
+ </div>
+ </div>
+ </div>
+</div>
+<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-grid #readyGrid [dataSource]="readySource"
+ (onRowActivate)="pickup($event)"
+ [sortable]="true" persistKey="booking.pull_list" >
+ <eg-grid-toolbar-action label="Pick up" i18n-label [action]="pickupSelected" [disableOnRows]="noSelectedRows"></eg-grid-toolbar-action>
+ <eg-grid-column name="id" [hidden]="true" [index]="true" i18n-label label="ID" path="id"></eg-grid-column>
+ <eg-grid-column name="barcode" label="Barcode" i18n-label path="current_resource.barcode"></eg-grid-column>
+ <eg-grid-column name="title" label="Title or name" i18n-label path="target_resource_type.name"></eg-grid-column>
+ <eg-grid-column label="Reservation start time" [datePlusTime]="true" path="reservations.0.start_time" i18n-label></eg-grid-column>
+ <eg-grid-column label="Reservation end time" [datePlusTime]="true" path="reservations.0.end_time" i18n-label></eg-grid-column>
+
+ </eg-grid>
+
+ <h2 class="text-center" i18n>Already picked up</h2>
+ <eg-reservations-grid #pickedUpGrid [patron]="patronId" status="pickedUp" persistSuffix="pickup.picked_up"></eg-reservations-grid>
+
+</div>
+
--- /dev/null
+import { Component, Input, OnInit, ViewChild } from '@angular/core';
+import { GridDataSource } from '@eg/share/grid/grid';
+import { Pager } from '@eg/share/util/pager';
+import {PatronService} from '@eg/staff/share/patron.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {AuthService} from '@eg/core/auth.service';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {Observable} from 'rxjs';
+import {tap} from 'rxjs/operators';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {ReservationsGridComponent} from './reservations-grid.component';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {ServerStoreService} from '@eg/core/server-store.service';
+
+
+@Component({
+ templateUrl: './pickup.component.html'
+})
+
+export class PickupComponent implements OnInit {
+ patronBarcode: string;
+ patronId: number;
+ retrievePatron: () => void;
+
+ @ViewChild('readyGrid') readyGrid: GridComponent;
+ @ViewChild('pickedUpGrid') pickedUpGrid: ReservationsGridComponent;
+
+ public readySource: GridDataSource;
+ noSelectedRows: (rows: IdlObject[]) => boolean;
+ pickupSelected: (reservations: IdlObject[]) => void;
+ pickup: (reservation: IdlObject) => Observable<any>;
+
+ onlyShowCaptured = true;
+ handleShowCapturedChange: () => void;
+
+ constructor(
+ private auth: AuthService,
+ private net: NetService,
+ private pcrud: PcrudService,
+ private patron: PatronService,
+ private route: ActivatedRoute,
+ private router: Router,
+ private store: ServerStoreService
+ ) {
+ }
+
+
+ ngOnInit() {
+ this.route.paramMap.subscribe((params: ParamMap) => {
+ this.patronId = +params.get('patron_id');
+ this.pcrud.search('au', {
+ 'id': this.patronId,
+ }, {
+ limit: 1,
+ flesh: 1,
+ flesh_fields: {'au': ['card']}
+ }).subscribe(
+ (resp) => { this.patronBarcode = resp.card().barcode(); },
+ (err) => { console.log(err); }
+ );
+ });
+
+ this.readySource = new GridDataSource();
+ this.readySource.getRows = (pager: Pager, sort: any[]) => {
+ const orderBy: any = {};
+ let where = {
+ 'usr' : this.patronId,
+ 'pickup_time' : null,
+ 'start_time' : {'!=': null},
+ 'cancel_time' : null
+ };
+ if (this.onlyShowCaptured) {
+ where['capture_time'] = {'!=': null};
+ }
+
+ return this.pcrud.search('bresv', where, {
+ order_by: orderBy,
+ limit: pager.limit,
+ offset: pager.offset,
+ flesh: 1,
+ flesh_fields: {'bresv' : [
+ 'usr', 'capture_staff', 'target_resource', 'target_resource_type', 'current_resource', 'request_lib', 'pickup_lib'
+ ] }
+ });
+
+ };
+ this.retrievePatron = () => {
+ if (this.patronBarcode) {
+ this.patron.bcSearch(this.patronBarcode).subscribe(
+ resp => { this.router.navigate(['/staff', 'booking', 'pickup', 'by_patron', resp[0].id]); }
+ );
+ }
+ };
+ this.noSelectedRows = (rows: IdlObject[]) => (rows.length === 0);
+
+ this.pickupSelected = (reservations: IdlObject[]) => {
+ const pickupOne = (thing: IdlObject) => {
+ if (!thing) { return; }
+ this.pickup(thing).subscribe(
+ () => pickupOne(reservations.shift()));
+ };
+ pickupOne(reservations.shift());
+ };
+
+ this.pickup = (reservation: IdlObject) => {
+ return this.net.request(
+ 'open-ils.circ',
+ 'open-ils.circ.reservation.pickup',
+ this.auth.token(),
+ {'patron_barcode': this.patronBarcode, 'reservation': reservation})
+ .pipe(tap(
+ (success) => {
+ this.readyGrid.reload();
+ this.pickedUpGrid.reloadGrid(); },
+ (error) => { console.log(error); }
+ ));
+ };
+ this.store.getItem('eg.booking.pickup.ready.only_show_captured').then(onlyCaptured => {
+ if (onlyCaptured != null) { this.onlyShowCaptured = onlyCaptured; }
+ });
+ this.handleShowCapturedChange = () => {
+ this.onlyShowCaptured = !this.onlyShowCaptured;
+ this.readyGrid.reload();
+ this.store.setItem('eg.booking.pickup.ready.only_show_captured', this.onlyShowCaptured);
+ };
+
+
+ }
+}
+
--- /dev/null
+{{dataSource | json }}
+<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>
+
+<div 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)="fill_grid()">
+ </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" class="form-control" id="days-hence" [(ngModel)]="daysHence" (ngModelChange)="fill_grid()">
+ </div>
+ </div>
+</div>
+<eg-grid [dataSource]="dataSource"
+ [sortable]="true" persistKey="booking.pull_list" >
+ <eg-grid-column name="id" [hidden]="true" [index]="true" i18n-label label="ID" path="id"></eg-grid-column>
+ <eg-grid-column name="barcode" label="Barcode" i18n-label path="current_resource.barcode"></eg-grid-column>
+ <eg-grid-column name="title" label="Title or name" i18n-label path="target_resource_type.name"></eg-grid-column>
+ <eg-grid-column label="Reservation start time" [datePlusTime]="true" path="reservations.0.start_time" i18n-label></eg-grid-column>
+ <eg-grid-column label="Reservation end time" [datePlusTime]="true" path="reservations.0.end_time" i18n-label></eg-grid-column>
+ <eg-grid-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>
+
--- /dev/null
+import { Component, Input, OnInit } from '@angular/core';
+import { GridDataSource } from '@eg/share/grid/grid';
+import { NetRequest, NetService } from '@eg/core/net.service';
+import { Pager } from '@eg/share/util/pager';
+import { AuthService } from '@eg/core/auth.service';
+
+@Component({
+ selector: 'eg-pull-list',
+ templateUrl: './pull-list.component.html'
+})
+
+export class PullListComponent implements OnInit {
+ @Input( ) daysHence: number;
+
+ public dataSource: GridDataSource;
+ private auth: AuthService;
+
+ constructor(
+ private net: NetService,
+ egAuth: AuthService
+ ) {
+ this.auth = egAuth;
+ this.daysHence = 5;
+ }
+
+ fill_grid () {
+ this.net.request(
+ 'open-ils.booking', 'open-ils.booking.reservations.get_pull_list',
+ this.auth.token(), null,
+ (86400 * this.daysHence), // convert seconds to days
+ 4
+ ).subscribe( data => {
+ this.dataSource.data = data;
+ });
+ }
+
+ ngOnInit() {
+ this.dataSource = new GridDataSource();
+ this.fill_grid();
+ }
+}
--- /dev/null
+<eg-grid #grid [dataSource]="gridSource"
+ (onRowActivate)="showEditDialog($event)"
+ [sortable]="true" persistKey="booking.{{persistSuffix}}" >
+ <eg-grid-toolbar-action label="Edit Selected" i18n-label [action]="editSelected" [disableOnRows]="noSelectedRows"></eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Cancel Selected" i18n-label [action]="cancelSelected" [disableOnRows]="cancelNotAppropriate"></eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Return Selected" i18n-label [action]="returnSelected" [disableOnRows]="returnNotAppropriate"></eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="View Reservations for This Patron" i18n-label [action]="viewByPatron" [disableOnRows]="notOnePatronSelected"></eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="View Reservations for This Resource" i18n-label [action]="viewByResource" [disableOnRows]="notOneResourceSelected"></eg-grid-toolbar-action>
+
+ <eg-grid-column name="id" [hidden]="true" [index]="true" i18n-label label="ID" path="id"></eg-grid-column>
+ <eg-grid-column label="Patron username" i18n-label path="usr.usrname"></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="Request library" i18n-label [hidden]="true" path="request_lib.name"></eg-grid-column>
+ <eg-grid-column label="Pickup library" i18n-label [hidden]="true" path="pickup_lib.name"></eg-grid-column>
+
+</eg-grid>
+
+<eg-fm-record-editor #editDialog
+ idlClass="bresv"
+ hiddenFields="xact_finish,cancel_time,booking_interval"
+ readonlyFields="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">
+</eg-fm-record-editor>
+<eg-confirm-dialog #confirmCancelReservationDialog
+ i18n-dialogTitle i18n-dialogBody
+ dialogTitle="Confirm Cancelation"
+ dialogBody="Are you sure you want to cancel {numRowsSelected, plural, =1 {this reservation} other {these {{numRowsSelected}} reservations}}?">
+</eg-confirm-dialog>
+<eg-no-timezone-set-dialog #noTimezoneSetDialog>
+</eg-no-timezone-set-dialog>
--- /dev/null
+import {Component, Input, OnInit, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs';
+import {tap} from 'rxjs/operators';
+import {AuthService} from '@eg/core/auth.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.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} 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 {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {NetService} from '@eg/core/net.service';
+import {NoTimezoneSetComponent} from './no-timezone-set.component';
+import {PatronService} from '@eg/staff/share/patron.service';
+
+import * as Moment from 'moment-timezone';
+
+@Component({
+ selector: 'eg-reservations-grid',
+ templateUrl: './reservations-grid.component.html',
+})
+export class ReservationsGridComponent implements OnInit {
+
+ @Input() patron: number;
+ @Input() resource: number;
+ @Input() resourceType: number;
+ @Input() status: 'pickedUp' | 'returnedToday';
+ @Input() persistSuffix: string;
+
+ gridSource: GridDataSource;
+ patronBarcode: string;
+ numRowsSelected: number;
+
+ @ViewChild('grid') grid: GridComponent;
+ @ViewChild('editDialog') editDialog: FmRecordEditorComponent;
+ @ViewChild('confirmCancelReservationDialog')
+ private _cancelReservationDialog: ConfirmDialogComponent;
+ @ViewChild('noTimezoneSetDialog') noTimezoneSetDialog: NoTimezoneSetComponent;
+
+ editSelected: (rows: IdlObject[]) => void;
+ returnSelected: (rows: IdlObject[]) => void;
+ returnResource: (rows: IdlObject) => Observable<any>;
+ cancelSelected: (rows: IdlObject[]) => void;
+ viewByPatron: (rows: IdlObject[]) => void;
+ viewByResource: (rows: IdlObject[]) => void;
+ filterByCurrentPatronBarcode: () => void;
+ filterByCurrentResourceBarcode: () => void;
+
+ reloadGrid: () => void;
+
+ noSelectedRows: (rows: IdlObject[]) => boolean;
+ notOnePatronSelected: (rows: IdlObject[]) => boolean;
+ notOneResourceSelected: (rows: IdlObject[]) => boolean;
+ cancelNotAppropriate: (rows: IdlObject[]) => boolean;
+ returnNotAppropriate: (rows: IdlObject[]) => boolean;
+
+ constructor(
+ private route: ActivatedRoute,
+ private router: Router,
+ private toast: ToastService,
+ private pcrud: PcrudService,
+ private auth: AuthService,
+ private format: FormatService,
+ private net: NetService,
+ private patronService: PatronService
+ ) {
+
+ }
+
+ ngOnInit() {
+ if (!(this.format.wsOrgTimezone)) {
+ this.noTimezoneSetDialog.open();
+ }
+
+ this.gridSource = new GridDataSource();
+
+ this.gridSource.getRows = (pager: Pager, sort: any[]) => {
+ const orderBy: any = {};
+ let where = {
+ 'usr' : (this.patron ? this.patron : {'>' : 0}),
+ 'target_resource_type' : (this.resourceType ? this.resourceType : {'>' : 0}),
+ 'cancel_time' : null,
+ 'xact_finish' : null,
+ };
+ if (this.resource) {
+ where['current_resource'] = this.resource;
+ }
+ if (this.status) {
+ if ('pickedUp' === this.status) {
+ where['pickup_time'] = {'!=': null};
+ where['return_time'] = null;
+ } else if ('returnedToday' === this.status) {
+ where['return_time'] = {'>': Moment().startOf('day').toISOString()};
+ }
+ }
+ 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'] }
+ });
+ };
+
+ this.editDialog.mode = 'update';
+ this.editSelected = (idlThings: IdlObject[]) => {
+ const editOneThing = (thing: IdlObject) => {
+ if (!thing) { return; }
+ this.showEditDialog(thing).then(
+ () => editOneThing(idlThings.shift()));
+ };
+ editOneThing(idlThings.shift()); };
+
+ this.cancelSelected = (reservations: IdlObject[]) => {
+ const reservationIds = reservations.map(reservation => reservation.id());
+ this.numRowsSelected = reservationIds.length;
+ this._cancelReservationDialog.open()
+ .then(
+ confirmed => {this.net.request(
+ 'open-ils.booking',
+ 'open-ils.booking.reservations.cancel',
+ this.auth.token(), reservationIds)
+ .subscribe(
+ (res) => this.handleSuccessfulCancel(res),
+ (err) => alert('ERR: ' + JSON.stringify(err))
+ );
+ },
+ dismissed => console.log('user cancelled'));
+ };
+
+ this.viewByPatron = (reservations: IdlObject[]) => {
+ const patronIds = reservations.map(reservation => reservation.usr().id());
+ this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_patron', patronIds[0]]);
+ };
+
+ this.viewByResource = (reservations: IdlObject[]) => {
+ const resourceBarcodes = reservations.map(reservation => reservation.current_resource().barcode());
+ this.filterByResourceBarcode(resourceBarcodes[0]);
+ };
+
+ this.noSelectedRows = (rows: IdlObject[]) => (rows.length === 0);
+ this.notOnePatronSelected = (rows: IdlObject[]) => (new Set(rows.map(row => row.usr().id())).size !== 1);
+ this.notOneResourceSelected = (rows: IdlObject[]) => (new Set(rows.map(row => row.current_resource().id())).size !== 1);
+ this.cancelNotAppropriate = (rows: IdlObject[]) => (this.noSelectedRows(rows) || ('pickedUp' === this.status));
+ this.returnNotAppropriate = (rows: IdlObject[]) => {
+ if (this.noSelectedRows(rows)) {
+ return true;
+ } else {
+ rows.forEach(row => {
+ if ((null == row.pickup_time()) || row.return_time()) { return true; }
+ });
+ }
+ return false;
+ }
+
+ this.reloadGrid = () => { this.grid.reload(); };
+
+ this.returnSelected = (reservations: IdlObject[]) => {
+ const returnOne = (thing: IdlObject) => {
+ if (!thing) { return; }
+ this.returnResource(thing).subscribe(
+ () => returnOne(reservations.shift()));
+ };
+ returnOne(reservations.shift());
+ };
+
+ 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(
+ (success) => { this.grid.reload(); },
+ (error) => { console.log(error); }
+ ));
+ };
+ }
+
+ showEditDialog(idlThing: IdlObject) {
+ this.editDialog.recId = idlThing.id();
+ return this.editDialog.open({size: 'lg'}).then(
+ ok => {
+ this.toast.success('Reservation successfully updated'); // TODO: needs i18n, pluralization
+ this.grid.reload();
+ },
+ err => {}
+ );
+ }
+
+ handleSuccessfulCancel(res: any) {
+ this.toast.success('Reservation successfully canceled'); // TODO: needs i18n, pluralization
+ this.grid.reload();
+ }
+ filterByResourceBarcode(barcode: string) {
+ this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_resource', barcode]);
+ }
+}
+
--- /dev/null
+import { Component, EventEmitter, OnInit, Input, Output } from '@angular/core';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+@Component({
+ selector: 'eg-resource-type-combobox',
+ template: `<eg-combobox
+ [attr.id]="domId"
+ placeholder="Resource type" i18n-placeholder
+ [entries]="resourceTypes"
+ (onChange)="onChange.emit($event)"
+ [startId]="startId"></eg-combobox>`
+})
+export class ResourceTypeComboboxComponent implements OnInit {
+
+ resourceTypes: ComboboxEntry[];
+
+ @Input() domId = '';
+ @Input() startId: number;
+ @Output() onChange: EventEmitter<ComboboxEntry>;
+
+ constructor(private pcrud: PcrudService) {
+ this.onChange = new EventEmitter<ComboboxEntry>();
+ }
+
+ ngOnInit() {
+ this.pcrud.retrieveAll('brt', {order_by: {brt: 'name'}})
+ .subscribe(type => {
+ if (!this.resourceTypes) { this.resourceTypes = []; }
+ this.resourceTypes.push({id: type.id(), label: type.name()});
+ });
+ }
+
+}
--- /dev/null
+<eg-staff-banner bannerText="Booking Return" i18n-bannerText>
+</eg-staff-banner>
+<eg-title i18n-prefix i18n-suffix prefix="Booking" suffix="Return"></eg-title>
+
+<!-- TODO: DRY This out: there has to be a good way to reuse the template -->
+<ngb-tabset (tabChange)="resetEverything()">
+ <ngb-tab title="By patron" i18n-title>
+ <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" [(ngModel)]="patronBarcode" (change)="retrievePatronByBarcode()">
+ </div>
+ </div>
+ </div>
+ </div>
+ <div *ngIf="patronId">
+ <h2 class="text-center" i18n>Ready for return</h2>
+ <eg-reservations-grid [patron]="patronId" status="pickedUp" persistSuffix="return.patron.picked_up"></eg-reservations-grid>
+
+ <h2 class="text-center" i18n>Returned today</h2>
+ <eg-reservations-grid [patron]="patronId" status="returnedToday" persistSuffix="return.patron.returned"></eg-reservations-grid>
+ </div>
+ </ng-template>
+ </ngb-tab>
+ <ngb-tab title="By resource" i18n-title>
+ <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" [(ngModel)]="resourceBarcode" (change)="retrievePatronByResource()">
+ </div>
+ </div>
+ <div *ngIf="patronId">
+ <h2 class="text-center" i18n>Ready for return</h2>
+ <eg-reservations-grid [resource]="patronId" status="pickedUp" persistSuffix="return.patron.picked_up"></eg-reservations-grid>
+
+ <h2 class="text-center" i18n>Returned today</h2>
+ <eg-reservations-grid [patron]="patronId" status="returnedToday" persistSuffix="return.patron.returned"></eg-reservations-grid>
+ </div>
+ </ng-template>
+ </ngb-tab>
+</ngb-tabset>
--- /dev/null
+import { Component, Input, OnInit } from '@angular/core';
+import { GridDataSource } from '@eg/share/grid/grid';
+import { Pager } from '@eg/share/util/pager';
+import {PatronService} from '@eg/staff/share/patron.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {AuthService} from '@eg/core/auth.service';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {Observable} from 'rxjs';
+
+
+@Component({
+ templateUrl: './return.component.html'
+})
+
+export class ReturnComponent implements OnInit {
+ resourceBarcode: string;
+ patronBarcode: string;
+ patronId: number;
+ retrievePatronByBarcode: () => void;
+ retrievePatronByResource: () => void;
+
+ noSelectedRows: (rows: IdlObject[]) => boolean;
+ resetEverything: () => void;
+
+ constructor(
+ private auth: AuthService,
+ private net: NetService,
+ private pcrud: PcrudService,
+ private patron: PatronService
+ ) {
+ }
+
+
+ ngOnInit() {
+ this.retrievePatronByBarcode = () => {
+ if (this.patronBarcode) {
+ this.patron.bcSearch(this.patronBarcode).subscribe(
+ resp => { this.patronId = resp[0]['id']; }
+ );
+ }
+ };
+ this.retrievePatronByResource = () => {
+ if (this.resourceBarcode) {
+ this.pcrud.search('brsrc', {'barcode': this.resourceBarcode}, {
+ 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}}}
+ }).subscribe((resp) => {
+ if (resp.curr_rsrcs().usr()) {
+ this.patronId = resp.curr_rsrcs().usr();
+ }
+ });
+ }
+ };
+ this.noSelectedRows = (rows: IdlObject[]) => (rows.length === 0);
+
+ this.resetEverything = () => {
+ this.resourceBarcode = null;
+ this.patronBarcode = null;
+ this.patronId = null;
+ };
+
+ }
+}
--- /dev/null
+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: '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: 'create_reservation',
+ component: CreateReservationComponent
+ }, {
+ path: 'pickup',
+ children: [
+ {path: '', component: PickupComponent},
+ {path: 'by_patron/:patron_id', component: PickupComponent},
+ ]}, {
+ path: 'pull_list',
+ component: PullListComponent
+ }, {
+ path: 'return',
+ component: ReturnComponent
+ },
+ ];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule],
+ providers: []
+})
+
+export class BookingRoutingModule {}
import {StringService} from '@eg/share/string/string.service';
import {TitleComponent} from '@eg/share/title/title.component';
import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {DateRangeSelectComponent} from '@eg/share/daterange-select/daterange-select.component';
import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
+import {DateTimeSelectComponent} from '@eg/share/datetime-select/datetime-select.component';
import {RecordBucketDialogComponent} from '@eg/staff/share/buckets/record-bucket-dialog.component';
import {BibSummaryComponent} from '@eg/staff/share/bib-summary/bib-summary.component';
import {TranslateComponent} from '@eg/staff/share/translate/translate.component';
TitleComponent,
OpChangeComponent,
FmRecordEditorComponent,
+ DateRangeSelectComponent,
DateSelectComponent,
+ DateTimeSelectComponent,
RecordBucketDialogComponent,
BibSummaryComponent,
TranslateComponent,
TitleComponent,
OpChangeComponent,
FmRecordEditorComponent,
+ DateRangeSelectComponent,
DateSelectComponent,
+ DateTimeSelectComponent,
RecordBucketDialogComponent,
BibSummaryComponent,
TranslateComponent,
<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>
<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>
redirectTo: 'splash',
pathMatch: 'full',
}, {
+ path: 'booking',
+ loadChildren : '@eg/staff/booking/booking.module#BookingModule'
+ }, {
path: 'about',
component: AboutComponent
}, {
[fieldOptions]="{marc_record_type:{customValues:[{id:'biblio'},{id:'serial'},{id:'authority'}]},description:{customTemplate:{template:descriptionTemplate,context:{'hello':'goodbye'}}}}"
recordId="1" orgDefaultAllowed="owner">
</eg-fm-record-editor>
- <button class="btn btn-dark" (click)="fmRecordEditor.open({size:'lg'})">
+ <button class="btn btn-dark" (click)="openEditor()">
Fm Record Editor
</button>
</div>
import {PrintService} from '@eg/share/print/print.service';
import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
import {FormatService} from '@eg/core/format.service';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
@Component({
templateUrl: 'sandbox.component.html'
@ViewChild('printTemplate')
private printTemplate: TemplateRef<any>;
+ @ViewChild('fmRecordEditor')
+ private fmRecordEditor: FmRecordEditorComponent;
+
// @ViewChild('helloStr') private helloStr: StringComponent;
gridDataSource: GridDataSource = new GridDataSource();
});
}
+ openEditor() {
+ this.fmRecordEditor.open({size: 'lg'}).then(
+ ok => { console.debug(ok); },
+ err => {
+ if (err && err.dismissed) {
+ console.debug('dialog was dismissed');
+ } else {
+ console.error(err);
+ }
+ }
+ );
+ }
+
btGridRowClassCallback(row: any): string {
if (row.id() === 1) {
return 'text-uppercase font-weight-bold text-danger';
--- /dev/null
+import {Injectable} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {Observable} from 'rxjs';
+
+
+@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);
+ }
+
+}
+
@Component({
selector: 'eg-staff-banner',
template:
- '<div class="lead alert alert-primary text-center pt-1 pb-1" role="alert">' +
- '<span>{{bannerText}}</span>' +
+ '<div class="alert alert-primary text-center pt-1 pb-1" role="alert">' +
+ '<h1 class="lead">{{bannerText}}</h1>' +
'</div>'
})
*/
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}
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);
)
);
-
-- server admin workstation settings
INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
VALUES (
'bool'
);
-
INSERT INTO config.usr_activity_type
(id, ewhat, ehow, egroup, enabled, transient, label)
VALUES (
oils_i18n_gettext(30, 'Generic Verify', 'cuat', 'label')
);
+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.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.booking.manage.filter', 'gui', 'string',
+ oils_i18n_gettext(
+ 'booking.manage.filter',
+ 'Sticky setting for filter tab in Manage 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')
+);
--- /dev/null
+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.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.booking.manage.filter', 'gui', 'string',
+ oils_i18n_gettext(
+ 'booking.manage.filter',
+ 'Sticky setting for filter tab in Manage 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;
--- /dev/null
+BEGIN;
+
+ALTER TABLE booking.reservation
+ ADD COLUMN note TEXT;
+
+COMMIT;
<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"
<li><a href ng-click="add_copies_to_bucket()">[% l('Add Items to Bucket') %]</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>
<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"
</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/pickup/by_patron/{{patron().id()}}" target="_top">
[% l('Booking: Pick Up Reservations') %]
</a>
</li>
</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>
[% 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>
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> " +
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));
});
}
+ $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';
});
}
+ $scope.manage_reservations = function() {
+ itemSvc.manage_reservations([$scope.args.copyBarcode]);
+ }
+
$scope.requestItems = function() {
itemSvc.requestItems([$scope.args.copyId]);
}
itemSvc.book_copies_now(copyGrid.selectedItems());
}
+ $scope.manage_reservations = function() {
+ var item = copyGrid.selectedItems()[0];
+ if (item)
+ itemSvc.manage_reservations(item.barcode);
+ }
+
$scope.requestItems = function() {
var copy_list = gatherSelectedHoldingsIds();
itemSvc.requestItems(copy_list);
});
}
+ service.manage_reservations = function(barcode) {
+ location.href = "/eg2/staff/booking/manage_reservations/by_resource/" + barcode;
+ }
+
service.requestItems = function(copy_list) {
if (copy_list.length == 0) return;
--- /dev/null
+Booking Module Refresh
+^^^^^^^^^^^^^^^^^^^^^^
+
+The Booking module has been redesigned, with many of its interfaces being
+redesigned in Angular.
+
+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
+----
+