From: Jane Sandberg Date: Mon, 25 Mar 2019 20:48:05 +0000 (+0000) Subject: LP1816475: Booking module refresh X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=35a1bc322a8a8570e8018fef17f7a5fda8e824a0;p=working%2FEvergreen.git LP1816475: Booking module refresh This commit ports several dojo interfaces to Angular(7). As part of this work, * Adds moment.js-based timezone support to the Angular fmeditor and grid * Adds a note field to booking.reservation * Adds a datetime-select widget to Angular * Adds a daterange-select widget to Angular * Adds usrname as a selector for actor.usr * Adds the new booking.reservation note field to the receipt in the dojo-based Capture Reservations screen * Allows the grid to disable saving * Adds a read-only display of au to the fm-editor * Adds a new patron service in staff/share Signed-off-by: Jane Sandberg --- diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 1f510735fb..f57c5ec82d 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -3651,7 +3651,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA - + @@ -5137,8 +5137,8 @@ SELECT usr, - - + + @@ -5154,6 +5154,7 @@ SELECT usr, + diff --git a/Open-ILS/src/eg2/package.json b/Open-ILS/src/eg2/package.json index 0da357937a..b256b9dd58 100644 --- a/Open-ILS/src/eg2/package.json +++ b/Open-ILS/src/eg2/package.json @@ -28,6 +28,8 @@ "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" diff --git a/Open-ILS/src/eg2/src/app/core/format.service.ts b/Open-ILS/src/eg2/src/app/core/format.service.ts index e788cd0e40..c764e61422 100644 --- a/Open-ILS/src/eg2/src/app/core/format.service.ts +++ b/Open-ILS/src/eg2/src/app/core/format.service.ts @@ -2,6 +2,7 @@ import {Injectable} from '@angular/core'; 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. @@ -16,6 +17,7 @@ export interface FormatParams { datatype?: string; orgField?: string; // 'shortname' || 'name' datePlusTime?: boolean; + timezoneContextOrg?: number; } @Injectable({providedIn: 'root'}) @@ -107,12 +109,15 @@ export class FormatService { 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); @@ -129,5 +134,17 @@ export class FormatService { return value + ''; } } + /** + * Create an IDL-friendly display version of a human-readable date + */ + idlFormatDate(original: string): string { + return Moment(original, 'MMM DD YYYY', 'America/Los_Angeles').format('YYYY-MM-DD'); + } + /** + * Create an IDL-friendly display version of a human-readable datetime + */ + idlFormatDatetime(original: string): string { + return Moment(original, 'MMM DD YYYY HH:mm', 'America/Los_Angeles').toISOString() + } } diff --git a/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.html b/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.html index 575bbde5c8..07a386cb38 100644 --- a/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.html +++ b/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.html @@ -1,7 +1,7 @@
-
diff --git a/Open-ILS/src/eg2/src/app/share/daterange-select/daterange-select.component.html b/Open-ILS/src/eg2/src/app/share/daterange-select/daterange-select.component.html new file mode 100644 index 0000000000..8dad08557d --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/daterange-select/daterange-select.component.html @@ -0,0 +1,18 @@ + + + + + + {{ date.day }} + + + +
+ +
From: {{ fromDate | json }} 
+
To: {{ toDate | json }} 
diff --git a/Open-ILS/src/eg2/src/app/share/daterange-select/daterange-select.component.ts b/Open-ILS/src/eg2/src/app/share/daterange-select/daterange-select.component.ts new file mode 100644 index 0000000000..72b297ce09 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/daterange-select/daterange-select.component.ts @@ -0,0 +1,61 @@ +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); + } +} diff --git a/Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.html b/Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.html new file mode 100644 index 0000000000..ee006604b5 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.html @@ -0,0 +1,48 @@ + +
+ +
+ +
+
+ + + + + + + + + {{ timezone || 'America/Los_Angeles'}} + + diff --git a/Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.ts b/Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.ts new file mode 100644 index 0000000000..e9101cdd40 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.ts @@ -0,0 +1,85 @@ +import { Component, Input, Output, EventEmitter, ViewChild, OnInit } from '@angular/core'; +import { ControlValueAccessor } from '@angular/forms'; +import {FormatService} from '@eg/core/format.service'; + +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() domId = ''; + @Input() fieldName: string; + @Input() required: boolean; + @Input() minuteStep: number; + @Input() showTZ = true; + @Input() timezone: string; + + @Input() initialIso: string; + + @Output() onChangeAsIso = new EventEmitter(); + + dateTime: any; // Used internally on internal input + timeModel: NgbTimeStruct; + dateModel: NgbDateStruct; + + constructor( + private format: FormatService + ) { + } + + ngOnInit() { + const start = this.initialIso ? Moment(this.initialIso) : Moment(); + this.setDefaultDate(start); + this.setDefaultTime(start); + + if (this.initialIso) { + 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 = this.format.transform({value: newDate, datatype: 'string', datePlusTime: true}); + // Update form passed in view value + this.onChangeAsIso.emit(Moment(newDate).toISOString); + } + } + + isDisabled(date: NgbDateStruct, current: { month: number }) { + return date.month !== current.month; + } + +} + diff --git a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html index aad65d15d6..2331740573 100644 --- a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html +++ b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html @@ -45,6 +45,14 @@ + + + + + + + + {{field.linkedValues[0].label}} + open_in_new + + + Cancel + diff --git a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts index 3e41fa221d..f78ead37bb 100644 --- a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts +++ b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts @@ -8,6 +8,8 @@ import {PcrudService} from '@eg/core/pcrud.service'; import {DialogComponent} from '@eg/share/dialog/dialog.component'; import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap'; import {ComboboxEntry} from '@eg/share/combobox/combobox.component'; +import {FormatService} from '@eg/core/format.service'; + interface CustomFieldTemplate { template: TemplateRef; @@ -109,6 +111,10 @@ export class FmRecordEditorComponent @Input() requiredFieldsList: string[] = []; @Input() requiredFields: string; // comma-separated string version + // list of timezone fields that should display with a timepicker + @Input() datetimeFieldsList: string[] = []; + @Input() datetimeFields: string; // comma-separated string version + // list of org_unit fields where a default value may be applied by // the org-select if no value is present. @Input() orgDefaultAllowedList: string[] = []; @@ -157,6 +163,7 @@ export class FmRecordEditorComponent private modal: NgbModal, // required for passing to parent private idl: IdlService, private auth: AuthService, + private format: FormatService, private pcrud: PcrudService) { super(modal); } @@ -192,6 +199,9 @@ export class FmRecordEditorComponent if (this.requiredFields) { this.requiredFieldsList = this.requiredFields.split(/,/); } + if (this.datetimeFields) { + this.datetimeFieldsList = this.datetimeFields.split(/,/); + } if (this.orgDefaultAllowed) { this.orgDefaultAllowedList = this.orgDefaultAllowed.split(/,/); } @@ -256,6 +266,8 @@ export class FmRecordEditorComponent } else { // TODO: some bools can be NULL rec[field.name]('f'); } + } else if ( field.datatype === 'timestamp' && field.datetime && rec[field.name]() ) { + rec[field.name] = this.format.idlFormatDatetime(rec[field.name]()) } else if (field.datatype === 'org_unit') { const org = rec[field.name](); if (org && typeof org === 'object') { @@ -355,6 +367,8 @@ export class FmRecordEditorComponent promise = this.wireUpCombobox(field); + } else if (field.datatype === 'timestamp') { + field.datetime = this.datetimeFieldsList.includes(field.name); } else if (field.datatype === 'org_unit') { field.orgDefaultAllowed = this.orgDefaultAllowedList.includes(field.name); @@ -465,6 +479,10 @@ export class FmRecordEditorComponent return 'template'; } + if ( field.datatype === 'timestamp' && field.datetime ) { + return 'timestamp-timepicker'; + } + // Some widgets handle readOnly for us. if ( field.datatype === 'timestamp' || field.datatype === 'org_unit' @@ -477,6 +495,10 @@ export class FmRecordEditorComponent return 'readonly-money'; } + if (field.datatype === 'link' && field.class === 'au') { + return 'readonly-au'; + } + if (field.datatype === 'link' || field.linkedValues) { return 'readonly-list'; } diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts index 4cebd48211..57f3afd950 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts +++ b/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts @@ -27,6 +27,9 @@ export class GridColumnComponent implements OnInit { // Display date and time when datatype = timestamp @Input() datePlusTime: boolean; + // Display using a specific OU's timestamp when datatype = timestamp + @Input() timezoneContextOrg: number; + // Used in conjunction with cellTemplate @Input() cellContext: any; @Input() cellTemplate: TemplateRef; @@ -54,6 +57,7 @@ export class GridColumnComponent implements OnInit { 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); } diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html index 5eaa81ff62..0c6ddca2b2 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html +++ b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html @@ -114,6 +114,7 @@ Manage Column Widths save Save Grid Settings diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.ts index 399a4c7211..1d33cd25aa 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.ts +++ b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.ts @@ -16,6 +16,7 @@ export class GridToolbarComponent implements OnInit { @Input() gridContext: GridContext; @Input() colWidthConfig: GridColumnWidthComponent; @Input() gridPrinter: GridPrintComponent; + @Input() disableSaveSettings = false; csvExportInProgress: boolean; csvExportUrl: SafeUrl; diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid.component.html index a98e17afaf..ad228b0bdd 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid.component.html +++ b/Open-ILS/src/eg2/src/app/share/grid/grid.component.html @@ -4,7 +4,8 @@ + [colWidthConfig]="colWidthConfig" + [disableSaveSettings]="disableSaveSettings"> diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts index 66686ef2e8..d4f6a7feb0 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts +++ b/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts @@ -94,6 +94,10 @@ export class GridComponent implements OnInit, AfterViewInit, OnDestroy { // 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. diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.ts index 3743488c39..9cff1606bb 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid.ts +++ b/Open-ILS/src/eg2/src/app/share/grid/grid.ts @@ -26,6 +26,7 @@ export class GridColumn { idlFieldDef: any; datatype: string; datePlusTime: boolean; + timezoneContextOrg: number; cellTemplate: TemplateRef; cellContext: any; isIndex: boolean; @@ -640,7 +641,8 @@ export class GridContext { idlClass: col.idlClass, idlField: col.idlFieldDef ? col.idlFieldDef.name : col.name, datatype: col.datatype, - datePlusTime: Boolean(col.datePlusTime) + datePlusTime: Boolean(col.datePlusTime), + timezoneContextOrg: Number(col.timezoneContextOrg) }); } diff --git a/Open-ILS/src/eg2/src/app/staff/booking/booking.module.ts b/Open-ILS/src/eg2/src/app/staff/booking/booking.module.ts new file mode 100644 index 0000000000..079a190f33 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/booking/booking.module.ts @@ -0,0 +1,32 @@ +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 { } + diff --git a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.html b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.html new file mode 100644 index 0000000000..dad65405a6 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.html @@ -0,0 +1,135 @@ +

Resources

+{{resources | json}} +

Grid

+{{scheduleSource.data | json}} + + + + +
+
+
+
+ +
+
+ + +
+ + +
+
+
+
+
+
+
+ +
+ + +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+ + +
+
+
Display options
+
    +
  • + + + + + + +
  • +
  • + + + + + + +
  • +
  • + + + + + + + + + + +
  • +
+
+
+
Filter by attributes
+
    +
  • + + + + + + + + + +
  • +
+
+
+ + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts new file mode 100644 index 0000000000..f48db2182e --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.ts @@ -0,0 +1,140 @@ +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'; + +import * as Moment from 'moment-timezone'; + + +@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; + + startTime: Moment; + endTime: Moment; + granularity: 15 | 30 | 60 = 30; + + scheduleSource: GridDataSource = new GridDataSource(); + + 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, + ) { + 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; + }; + } + + + ngOnInit() { + this.dateLimiter.initialDate = new Date(); + + + this.isBooked = (row: any, col: any) => { + if ((col.name !== 'time') && (row[col.name])) { + return 'bg-warning'; + } + }; + + this.limitByAttr = (attributeId: number, $event: ComboboxEntry) => { + console.log('LIMIT'); + console.log('id: ' + attributeId); + console.log('event: ' + JSON.stringify($event)); + }; + + } + 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 => {} + ); + } + 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); + }, err => { + console.debug(err); + }, () => { + this.fetchData('type', event.id); + }); + } + } + + 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 = []; + this.startTime = Moment(new Date( + this.dateLimiter.current.year, + this.dateLimiter.current.month - 1, + this.dateLimiter.current.day, + 9, 0, 0), 'Asia/Tokyo'); + this.endTime = Moment(new Date( + this.dateLimiter.current.year, + this.dateLimiter.current.month - 1, + this.dateLimiter.current.day, + 17, 0, 0), 'Asia/Tokyo'); + let currentTime = this.startTime; + while (currentTime < this.endTime) { + this.scheduleSource.data.push({ + 'time': currentTime.format('LT'), + 'ROOM1231': 'Professor Pickle' + }); + currentTime.minute(currentTime.minute() + this.granularity); + } + } + +} + diff --git a/Open-ILS/src/eg2/src/app/staff/booking/manage-reservations.component.html b/Open-ILS/src/eg2/src/app/staff/booking/manage-reservations.component.html new file mode 100644 index 0000000000..42ec2cab65 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/booking/manage-reservations.component.html @@ -0,0 +1,88 @@ + + + + +
+
+
+
+ +
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+

Filter reservations

+ + + + filter_list Filter by patron + + +
+
+
+ +
+ +
+ +
+
+
+
+
+ + + filter_list Filter by resource + + +
+
+
+ +
+ +
+ +
+
+
+
+
+ + + filter_list Filter by resource type + + +
+
+
+ +
+ +
+ +
+
+
+
+
+
+
+
+
+ + diff --git a/Open-ILS/src/eg2/src/app/staff/booking/manage-reservations.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/manage-reservations.component.ts new file mode 100644 index 0000000000..fa2d2bd5a4 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/booking/manage-reservations.component.ts @@ -0,0 +1,125 @@ +import {Component, OnInit, ViewChild} from '@angular/core'; +import {Router, ActivatedRoute, ParamMap} from '@angular/router'; +import {single} from 'rxjs/operators'; +import { NgbTabChangeEvent } from '@ng-bootstrap/ng-bootstrap'; +import {ComboboxEntry} from '@eg/share/combobox/combobox.component'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {PatronService} from '@eg/staff/share/patron.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {ReservationsGridComponent} from './reservations-grid.component'; +import {ServerStoreService} from '@eg/core/server-store.service'; +import {ToastService} from '@eg/share/toast/toast.service'; + +@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; + setStickyFilter: ($event: NgbTabChangeEvent) => void; + + constructor( + private route: ActivatedRoute, + private router: Router, + private pcrud: PcrudService, + private patron: PatronService, + private store: ServerStoreService, + private toast: ToastService + ) { + } + + 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'); + + if (this.patronId) { + this.pcrud.search('au', { + 'id': this.patronId, + }, { + limit: 1, + flesh: 1, + flesh_fields: {'au': ['card']} + }).subscribe( + (resp) => { + this.reservationsGrid.reloadGrid(); + this.patronBarcode = resp.card().barcode(); }, + (err) => { console.debug(err); } + ); + } else if (this.resourceBarcode) { + this.selectedFilter = 'resource'; + this.pcrud.search('brsrc', + {'barcode' : this.resourceBarcode}, {'limit': 1}) + .pipe(single()) + .subscribe((res) => { + this.resourceId = res.id(); + this.reservationsGrid.reloadGrid(); + }, (err) => { + this.resourceId = -1; + this.toast.danger('No resource found with this barcode'); + }); + } else if (this.resourceTypeId) { + this.selectedFilter = 'type'; + this.reservationsGrid.reloadGrid(); + } + + if (!(this.patronId)) { + this.store.getItem('eg.booking.manage.filter').then(filter => { + if (filter) { this.selectedFilter = filter; } + }); + } + }); + + + this.setStickyFilter = ($event: NgbTabChangeEvent) => { + this.store.setItem('eg.booking.manage.filter', $event.nextId); + }; + + this.removeFilters = () => { + this.router.navigate(['/staff', 'booking', 'manage_reservations']); + }; + + this.filterByCurrentPatronBarcode = () => { + if (this.patronBarcode) { + this.patron.bcSearch(this.patronBarcode).pipe(single()).subscribe( + (response) => { + this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_patron', response[0].id]); + }, (error) => { + this.toast.danger('No patron found with this barcode'); + }); + } 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]); + } + }; + + } +} + diff --git a/Open-ILS/src/eg2/src/app/staff/booking/no-timezone-set.component.html b/Open-ILS/src/eg2/src/app/staff/booking/no-timezone-set.component.html new file mode 100644 index 0000000000..0abaad5a93 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/booking/no-timezone-set.component.html @@ -0,0 +1,17 @@ + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/booking/no-timezone-set.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/no-timezone-set.component.ts new file mode 100644 index 0000000000..2d6282f2aa --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/booking/no-timezone-set.component.ts @@ -0,0 +1,16 @@ +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'); + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/booking/pickup.component.html b/Open-ILS/src/eg2/src/app/staff/booking/pickup.component.html new file mode 100644 index 0000000000..34b236f838 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/booking/pickup.component.html @@ -0,0 +1,37 @@ + + + + +
+
+
+
+ + +
+
+
+
+
+

Ready for pickup

+
+ + +
+ + + + + + + + + + +

Already picked up

+ + +
+ diff --git a/Open-ILS/src/eg2/src/app/staff/booking/pickup.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/pickup.component.ts new file mode 100644 index 0000000000..57b54b1418 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/booking/pickup.component.ts @@ -0,0 +1,137 @@ +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 {single, 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'; +import {ToastService} from '@eg/share/toast/toast.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; + + 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, + private toast: ToastService + ) { + } + + + 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(); + this.readyGrid.reload(); + this.pickedUpGrid.reloadGrid(); + }, (err) => { console.debug(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).pipe(single()).subscribe( + resp => { this.router.navigate(['/staff', 'booking', 'pickup', 'by_patron', resp[0].id]); }, + err => { this.toast.danger('No patron found with this barcode'); }, + ); + } + }; + 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.debug(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); + }; + + + } +} + diff --git a/Open-ILS/src/eg2/src/app/staff/booking/pull-list.component.html b/Open-ILS/src/eg2/src/app/staff/booking/pull-list.component.html new file mode 100644 index 0000000000..af326d9352 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/booking/pull-list.component.html @@ -0,0 +1,36 @@ +{{dataSource | json }} + + + + +
+
+
+
+ +
+ + +
+
+
+
+
+ +
+ +
+
+
+ + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/booking/pull-list.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/pull-list.component.ts new file mode 100644 index 0000000000..11a13a287f --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/booking/pull-list.component.ts @@ -0,0 +1,41 @@ +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(); + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.html b/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.html new file mode 100644 index 0000000000..7b48afa97c --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.html @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts new file mode 100644 index 0000000000..6ab727f296 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts @@ -0,0 +1,207 @@ +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; + 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)) + ); + }); + }; + + 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.debug(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]); + } +} + diff --git a/Open-ILS/src/eg2/src/app/staff/booking/resource-type-combobox.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/resource-type-combobox.component.ts new file mode 100644 index 0000000000..fe12178370 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/booking/resource-type-combobox.component.ts @@ -0,0 +1,34 @@ +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: `` +}) +export class ResourceTypeComboboxComponent implements OnInit { + + resourceTypes: ComboboxEntry[]; + + @Input() domId = ''; + @Input() startId: number; + @Output() typeChanged: EventEmitter; + + constructor(private pcrud: PcrudService) { + this.typeChanged = new EventEmitter(); + } + + 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()}); + }); + } + +} diff --git a/Open-ILS/src/eg2/src/app/staff/booking/return.component.html b/Open-ILS/src/eg2/src/app/staff/booking/return.component.html new file mode 100644 index 0000000000..1743504155 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/booking/return.component.html @@ -0,0 +1,45 @@ + + + + + + + + +
+
+
+
+ + +
+
+
+
+
+

Ready for return

+ + +

Returned today

+ +
+
+
+ + +
+
+ + +
+
+
+

Ready for return

+ + +

Returned today

+ +
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/booking/return.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/return.component.ts new file mode 100644 index 0000000000..20d8b3549d --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/booking/return.component.ts @@ -0,0 +1,68 @@ +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'; +import {single} from 'rxjs/operators'; + + +@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).pipe(single()).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; + }; + + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/booking/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/booking/routing.module.ts new file mode 100644 index 0000000000..b50d26ca7c --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/booking/routing.module.ts @@ -0,0 +1,39 @@ +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 {} diff --git a/Open-ILS/src/eg2/src/app/staff/common.module.ts b/Open-ILS/src/eg2/src/app/staff/common.module.ts index 5a83f8ac29..78db307f38 100644 --- a/Open-ILS/src/eg2/src/app/staff/common.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/common.module.ts @@ -16,7 +16,9 @@ import {StringComponent} from '@eg/share/string/string.component'; 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'; @@ -39,7 +41,9 @@ import {AdminPageComponent} from '@eg/staff/share/admin-page/admin-page.componen TitleComponent, OpChangeComponent, FmRecordEditorComponent, + DateRangeSelectComponent, DateSelectComponent, + DateTimeSelectComponent, RecordBucketDialogComponent, BibSummaryComponent, TranslateComponent, @@ -63,7 +67,9 @@ import {AdminPageComponent} from '@eg/staff/share/admin-page/admin-page.componen TitleComponent, OpChangeComponent, FmRecordEditorComponent, + DateRangeSelectComponent, DateSelectComponent, + DateTimeSelectComponent, RecordBucketDialogComponent, BibSummaryComponent, TranslateComponent, diff --git a/Open-ILS/src/eg2/src/app/staff/nav.component.html b/Open-ILS/src/eg2/src/app/staff/nav.component.html index 8034f4174e..0aa6dae9dd 100644 --- a/Open-ILS/src/eg2/src/app/staff/nav.component.html +++ b/Open-ILS/src/eg2/src/app/staff/nav.component.html @@ -294,7 +294,7 @@ pin_drop Capture Resources
- + trending_up Pick Up Reservations @@ -302,6 +302,10 @@ trending_down Return Reservations + + edit_attributes + Manage Reservations + diff --git a/Open-ILS/src/eg2/src/app/staff/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/routing.module.ts index 6f20336660..e390a3db4d 100644 --- a/Open-ILS/src/eg2/src/app/staff/routing.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/routing.module.ts @@ -19,6 +19,9 @@ const routes: Routes = [{ redirectTo: 'splash', pathMatch: 'full', }, { + path: 'booking', + loadChildren : '@eg/staff/booking/booking.module#BookingModule' + }, { path: 'about', component: AboutComponent }, { diff --git a/Open-ILS/src/eg2/src/app/staff/share/patron.service.ts b/Open-ILS/src/eg2/src/app/staff/share/patron.service.ts new file mode 100644 index 0000000000..b11626c71d --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/share/patron.service.ts @@ -0,0 +1,23 @@ +import {Injectable} from '@angular/core'; +import {NetService} from '@eg/core/net.service'; +import {AuthService} from '@eg/core/auth.service'; +import {Observable} from 'rxjs'; + + +@Injectable() +export class PatronService { + constructor( + private net: NetService, + private auth: AuthService + ) {} + + bcSearch(barcode: string): Observable { + return this.net.request( + 'open-ils.actor', + 'open-ils.actor.get_barcodes', + this.auth.token(), this.auth.user().ws_ou(), + 'actor', barcode); + } + +} + diff --git a/Open-ILS/src/eg2/src/app/staff/share/staff-banner.component.ts b/Open-ILS/src/eg2/src/app/staff/share/staff-banner.component.ts index 13ac684e34..8c1053434c 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/staff-banner.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/staff-banner.component.ts @@ -3,8 +3,8 @@ import {Component, OnInit, Input} from '@angular/core'; @Component({ selector: 'eg-staff-banner', template: - '