From: Jane Sandberg Date: Sun, 17 Nov 2019 04:50:00 +0000 (-0800) Subject: LP1851306: Port Capture Booking Resource to Angular X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=5bc6611bc0255d8d982d92286fe210746c30b5be;p=Evergreen.git LP1851306: Port Capture Booking Resource to Angular Signed-off-by: Jane Sandberg Signed-off-by: Terran McCanna Signed-off-by: Galen Charlton --- 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 index dbcfb03b8f..bb3d1b1006 100644 --- a/Open-ILS/src/eg2/src/app/staff/booking/booking.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/booking/booking.module.ts @@ -3,6 +3,7 @@ import {ReactiveFormsModule} from '@angular/forms'; import {StaffCommonModule} from '@eg/staff/common.module'; import {BookingRoutingModule} from './routing.module'; import {CancelReservationDialogComponent} from './cancel-reservation-dialog.component'; +import {CaptureComponent} from './capture.component'; import {CreateReservationComponent} from './create-reservation.component'; import {CreateReservationDialogComponent} from './create-reservation-dialog.component'; import {ManageReservationsComponent} from './manage-reservations.component'; @@ -28,6 +29,7 @@ import {OrgFamilySelectModule} from '@eg/share/org-family-select/org-family-sele ], declarations: [ CancelReservationDialogComponent, + CaptureComponent, CreateReservationComponent, CreateReservationDialogComponent, ManageReservationsComponent, diff --git a/Open-ILS/src/eg2/src/app/staff/booking/capture.component.html b/Open-ILS/src/eg2/src/app/staff/booking/capture.component.html new file mode 100644 index 0000000000..b840f4140c --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/booking/capture.component.html @@ -0,0 +1,22 @@ + + + + + + + + +
+
+
+
+ + +
+
+
+
+ +

Captured today

+ + diff --git a/Open-ILS/src/eg2/src/app/staff/booking/capture.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/capture.component.ts new file mode 100644 index 0000000000..5e4b7b3cf4 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/booking/capture.component.ts @@ -0,0 +1,89 @@ +import {Component, OnInit, OnDestroy, ViewChild} from '@angular/core'; +import {FormGroup, FormControl} from '@angular/forms'; +import {of, Subscription} from 'rxjs'; +import {debounceTime, single, switchMap, tap} from 'rxjs/operators'; +import {AuthService} from '@eg/core/auth.service'; +import {NetService} from '@eg/core/net.service'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {BookingResourceBarcodeValidator} from './booking_resource_validator.directive'; +import {ReservationActionsService} from './reservation-actions.service'; +import {ReservationsGridComponent} from './reservations-grid.component'; + +@Component({ + templateUrl: './capture.component.html' +}) + +export class CaptureComponent implements OnInit, OnDestroy { + + findResource: FormGroup; + subscriptions: Subscription[] = []; + + @ViewChild('capturedTodayGrid', { static: false }) capturedTodayGrid: ReservationsGridComponent; + @ViewChild('noResourceString', { static: true }) noResourceString: StringComponent; + @ViewChild('captureSuccessString', { static: true }) captureSuccessString: StringComponent; + @ViewChild('captureFailureString', { static: true }) captureFailureString: StringComponent; + + constructor( + private auth: AuthService, + private net: NetService, + private resourceValidator: BookingResourceBarcodeValidator, + private toast: ToastService, + private actions: ReservationActionsService + ) { + } + + ngOnInit() { + this.findResource = new FormGroup({ + 'resourceBarcode': new FormControl(null, [], this.resourceValidator.validate) + }); + + const debouncing = 1500; + this.subscriptions.push( + this.resourceBarcode.valueChanges.pipe( + debounceTime(debouncing), + switchMap((val) => { + if ('INVALID' === this.resourceBarcode.status) { + this.noResourceString.current() + .then(str => this.toast.danger(str)); + return of(); + } else { + return this.net.request( 'open-ils.booking', + 'open-ils.booking.resources.capture_for_reservation', + this.auth.token(), this.resourceBarcode.value ) + .pipe(switchMap((result: any) => { + if (result && result.ilsevent !== undefined) { + if (result.payload && result.payload.captured > 0) { + this.captureSuccessString.current() + .then(str => this.toast.success(str)); + this.actions.printCaptureSlip(result.payload); + this.capturedTodayGrid.reloadGrid(); + } else { + this.captureFailureString.current() + .then(str => this.toast.danger(str)); + } + } else { + this.captureFailureString.current() + .then(str => this.toast.danger(str)); + } + return of(); + })); + } + }) + ) + .subscribe()); + + } + + get resourceBarcode() { + return this.findResource.get('resourceBarcode'); + } + + + ngOnDestroy(): void { + this.subscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + } + +} diff --git a/Open-ILS/src/eg2/src/app/staff/booking/reservation-actions.service.ts b/Open-ILS/src/eg2/src/app/staff/booking/reservation-actions.service.ts index 5545d06225..79f5b33994 100644 --- a/Open-ILS/src/eg2/src/app/staff/booking/reservation-actions.service.ts +++ b/Open-ILS/src/eg2/src/app/staff/booking/reservation-actions.service.ts @@ -1,14 +1,33 @@ import {Injectable} from '@angular/core'; import {Router} from '@angular/router'; +import {Observable, of} from 'rxjs'; +import {mergeMap, switchMap, tap} from 'rxjs/operators'; +import {IdlObject} from '@eg/core/idl.service'; +import {AuthService} from '@eg/core/auth.service'; +import {PrintService} from '@eg/share/print/print.service'; import {PcrudService} from '@eg/core/pcrud.service'; // Some grid actions that are shared across booking grids +export interface CaptureInformation { + captured: number; + reservation: IdlObject; + mvr?: IdlObject; + new_copy_status?: number; + transit?: IdlObject; + resource?: IdlObject; + type?: IdlObject; + staff?: IdlObject; + workstation?: string; +} + @Injectable({providedIn: 'root'}) export class ReservationActionsService { constructor( + private auth: AuthService, private pcrud: PcrudService, + private printer: PrintService, private router: Router, ) { } @@ -17,6 +36,21 @@ export class ReservationActionsService { this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_resource', barcode]); } + printCaptureSlip = (templateData: CaptureInformation) => { + templateData.staff = this.auth.user(); + templateData.workstation = this.auth.workstation(); + this.printer.print({ + templateName: 'booking_capture', + contextData: templateData, + printContext: 'receipt' + }); + } + + reprintCaptureSlip = (ids: number[]): Observable => { + return this.fetchDataForCaptureSlip$(ids) + .pipe(tap((data) => this.printCaptureSlip(data))); + } + viewItemStatus = (barcode: string) => { this.pcrud.search('acp', { 'barcode': barcode }, { limit: 1 }) .subscribe((acp) => { @@ -28,5 +62,34 @@ export class ReservationActionsService { return (new Set(ids).size !== 1); } + private fetchDataForCaptureSlip$ = (ids: number[]): Observable => { + return this.pcrud.search('bresv', {'id': ids}, { + flesh: 2, + flesh_fields : { + 'bresv': ['usr', 'current_resource', 'type'], + 'au': ['card'], + 'brsrc': ['type'] + } + }).pipe(mergeMap((reservation: IdlObject) => this.assembleDataForCaptureSlip$(reservation))); + } + + private assembleDataForCaptureSlip$ = (reservation: IdlObject): Observable => { + let observable$ = of({ + reservation: reservation, + captured: 1 + }); + if (reservation.pickup_lib() === this.auth.user().ws_ou()) { + observable$ = this.pcrud.search('artc', {'reservation': reservation.id()}, {limit: 1}) + .pipe(switchMap(transit => { + return of({ + reservation: reservation, + captured: 1, + transit: transit + }); + })); + } + return observable$; + } + } diff --git a/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.html b/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.html index b8b5b51e8d..42f4b3eef3 100644 --- a/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.html +++ b/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.html @@ -5,6 +5,7 @@ persistKey="booking.{{persistSuffix}}" > + diff --git a/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts index 4e75a61503..79caf7ce75 100644 --- a/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts @@ -31,7 +31,7 @@ export class ReservationsGridComponent implements OnChanges, OnInit { @Input() resourceBarcode: string; @Input() resourceType: number; @Input() pickupLibIds: number[]; - @Input() status: 'pickupReady' | 'pickedUp' | 'returnReady' | 'returnedToday'; + @Input() status: 'capturedToday' | 'pickupReady' | 'pickedUp' | 'returnReady' | 'returnedToday'; @Input() persistSuffix: string; @Input() onlyCaptured = false; @@ -51,6 +51,7 @@ export class ReservationsGridComponent implements OnChanges, OnInit { editSelected: (rows: IdlObject[]) => void; pickupSelected: (rows: IdlObject[]) => void; pickupResource: (rows: IdlObject) => Observable; + reprintCaptureSlip: (rows: IdlObject[]) => void; returnSelected: (rows: IdlObject[]) => void; returnResource: (rows: IdlObject) => Observable; cancelSelected: (rows: IdlObject[]) => void; @@ -63,14 +64,13 @@ export class ReservationsGridComponent implements OnChanges, OnInit { handleRowActivate: (row: IdlObject) => void; redirectToCreate: () => void; - reloadGrid: () => void; - noSelectedRows: (rows: IdlObject[]) => boolean; notOnePatronSelected: (rows: IdlObject[]) => boolean; notOneResourceSelected: (rows: IdlObject[]) => boolean; notOneCatalogedItemSelected: (rows: IdlObject[]) => boolean; cancelNotAppropriate: (rows: IdlObject[]) => boolean; pickupNotAppropriate: (rows: IdlObject[]) => boolean; + reprintNotAppropriate: (rows: IdlObject[]) => boolean; editNotAppropriate: (rows: IdlObject[]) => boolean; returnNotAppropriate: (rows: IdlObject[]) => boolean; @@ -122,6 +122,9 @@ export class ReservationsGridComponent implements OnChanges, OnInit { where['return_time'] = null; } else if ('returnedToday' === this.status) { where['return_time'] = {'>': Moment().startOf('day').toISOString()}; + } else if ('capturedToday' === this.status) { + where['capture_time'] = {'between': [Moment().startOf('day').toISOString(), + Moment().add(1, 'day').startOf('day').toISOString()]}; } } else { where['return_time'] = null; @@ -185,12 +188,23 @@ export class ReservationsGridComponent implements OnChanges, OnInit { }; this.cancelNotAppropriate = (rows: IdlObject[]) => (this.noSelectedRows(rows) || ['pickedUp', 'returnReady', 'returnedToday'].includes(this.status)); - this.pickupNotAppropriate = (rows: IdlObject[]) => (this.noSelectedRows(rows) || ('pickupReady' !== this.status)); + this.pickupNotAppropriate = (rows: IdlObject[]) => + (this.noSelectedRows(rows) || !('pickupReady' === this.status || 'capturedToday' === this.status)); this.editNotAppropriate = (rows: IdlObject[]) => (this.noSelectedRows(rows) || ('returnedToday' === this.status)); + this.reprintNotAppropriate = (rows: IdlObject[]) => { + if (this.noSelectedRows(rows)) { + return true; + } else if ('capturedToday' === this.status) { + return false; + } else if (rows.filter(row => !(row.capture_time())).length) { // If any of the rows have not been captured + return true; + } + return false; + }; this.returnNotAppropriate = (rows: IdlObject[]) => { if (this.noSelectedRows(rows)) { return true; - } else if (this.status && ('pickupReady' === this.status)) { + } else if (this.status && ('pickupReady' === this.status || 'capturedToday' === this.status)) { return true; } else { rows.forEach(row => { @@ -200,8 +214,6 @@ export class ReservationsGridComponent implements OnChanges, OnInit { return false; }; - this.reloadGrid = () => { this.grid.reload(); }; - this.pickupSelected = (reservations: IdlObject[]) => { const pickupOne = (thing: IdlObject) => { if (!thing) { return; } @@ -220,6 +232,10 @@ export class ReservationsGridComponent implements OnChanges, OnInit { returnOne(reservations.shift()); }; + this.reprintCaptureSlip = (reservations: IdlObject[]) => { + this.actions.reprintCaptureSlip(reservations.map((r) => r.id())).subscribe(); + }; + this.pickupResource = (reservation: IdlObject) => { return this.net.request( 'open-ils.circ', @@ -278,6 +294,8 @@ export class ReservationsGridComponent implements OnChanges, OnInit { ngOnChanges() { this.reloadGrid(); } + reloadGrid() { this.grid.reload(); } + enrichRow$ = (row: IdlObject): Observable => { return from(this.org.settings('lib.timezone', row.pickup_lib().id())).pipe( switchMap((tz) => { 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 index bc12e96a45..75e34ba7e3 100644 --- a/Open-ILS/src/eg2/src/app/staff/booking/routing.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/booking/routing.module.ts @@ -1,5 +1,6 @@ import {NgModule} from '@angular/core'; import {RouterModule, Routes} from '@angular/router'; +import {CaptureComponent} from './capture.component'; import {CreateReservationComponent} from './create-reservation.component'; import {ManageReservationsComponent} from './manage-reservations.component'; import {PickupComponent} from './pickup.component'; @@ -20,6 +21,9 @@ const routes: Routes = [{ {path: 'by_resource/:resource_barcode', component: ManageReservationsComponent}, {path: 'by_resource_type/:resource_type_id', component: ManageReservationsComponent}, ]}, { + path: 'capture', + component: CaptureComponent + }, { path: 'pickup', children: [ {path: '', component: PickupComponent}, 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 5310b5b340..b6641b4b44 100644 --- a/Open-ILS/src/eg2/src/app/staff/nav.component.html +++ b/Open-ILS/src/eg2/src/app/staff/nav.component.html @@ -318,7 +318,7 @@ list Pull List - + pin_drop Capture Resources diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql index b6959bb20e..6370049352 100644 --- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql +++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql @@ -20280,6 +20280,42 @@ $TEMPLATE$ $TEMPLATE$ ); +INSERT INTO config.print_template + (id, name, locale, active, owner, label, template) +VALUES ( + 3, 'booking_capture', 'en-US', TRUE, + (SELECT id FROM actor.org_unit WHERE parent_ou IS NULL), + oils_i18n_gettext(3, 'Booking capture slip', 'cpt', 'label'), +$TEMPLATE$ +[%- + USE date; + SET data = template_data; + # template_data is data returned from open-ils.booking.resources.capture_for_reservation. +-%] +
+ [% IF data.transit %] +
This item need to be routed to data.transit.dest
+ [% ELSE %] +
This item need to be routed to RESERVATION SHELF:
+ [% END %] +
Barcode: [% data.reservation.current_resource.barcode %]
+
Title: [% data.reservation.current_resource.type.name %]
+
Note: [% data.reservation.note %]
+
+

Reserved for patron [% data.reservation.usr.family_name %], [% data.reservation.usr.first_given_name %] [% data.reservation.usr.second_given_name %] +
Barcode: [% data.reservation.usr.card.barcode %]

+

Request time: [% date.format(helpers.format_date(data.reservation.request_time, client_timezone), '%x %r', locale) %] +
Reserved from: + [% date.format(helpers.format_date(data.reservation.start_time, client_timezone), '%x %r', locale) %] + - [% date.format(helpers.format_date(data.reservation.end_time, client_timezone), '%x %r', locale) %]

+

Slip date: [% date.format(helpers.current_date(client_timezone), '%x %r', locale) %]
+ Printed by [% data.staff.family_name %], [% data.staff.first_given_name %] [% data.staff.second_given_name %] + at [% data.workstation %]

+
+
+ +$TEMPLATE$ +); -- Allow for 1k stock templates SELECT SETVAL('config.print_template_id_seq'::TEXT, 1000); @@ -20328,6 +20364,12 @@ VALUES ( 'Grid Config: Booking Return Resource tab Returned Today grid', 'cwst', 'label') ), ( + 'eg.grid.booking.captured', 'gui', 'object', + oils_i18n_gettext( + 'booking.manage', + 'Grid Config: Booking Captured Reservations', + 'cwst', 'label') +), ( 'eg.booking.manage.selected_org_family', 'gui', 'object', oils_i18n_gettext( 'booking.manage.selected_org_family', diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.port_capture_reservations_to_angular.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.port_capture_reservations_to_angular.sql new file mode 100644 index 0000000000..fbf53fc4ff --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.port_capture_reservations_to_angular.sql @@ -0,0 +1,52 @@ +BEGIN; + +SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version); + +INSERT INTO config.print_template + (id, name, locale, active, owner, label, template) +VALUES ( + 3, 'booking_capture', 'en-US', TRUE, + (SELECT id FROM actor.org_unit WHERE parent_ou IS NULL), + oils_i18n_gettext(3, 'Booking capture slip', 'cpt', 'label'), +$TEMPLATE$ +[%- + USE date; + SET data = template_data; + # template_data is data returned from open-ils.booking.resources.capture_for_reservation. +-%] +
+ [% IF data.transit %] +
This item need to be routed to data.transit.dest
+ [% ELSE %] +
This item need to be routed to RESERVATION SHELF:
+ [% END %] +
Barcode: [% data.reservation.current_resource.barcode %]
+
Title: [% data.reservation.current_resource.type.name %]
+
Note: [% data.reservation.note %]
+
+

Reserved for patron [% data.reservation.usr.family_name %], [% data.reservation.usr.first_given_name %] [% data.reservation.usr.second_given_name %] +
Barcode: [% data.reservation.usr.card.barcode %]

+

Request time: [% date.format(helpers.format_date(data.reservation.request_time, client_timezone), '%x %r', locale) %] +
Reserved from: + [% date.format(helpers.format_date(data.reservation.start_time, client_timezone), '%x %r', locale) %] + - [% date.format(helpers.format_date(data.reservation.end_time, client_timezone), '%x %r', locale) %]

+

Slip date: [% date.format(helpers.current_date(client_timezone), '%x %r', locale) %]
+ Printed by [% data.staff.family_name %], [% data.staff.first_given_name %] [% data.staff.second_given_name %] + at [% data.workstation %]

+
+
+ +$TEMPLATE$ +); + +INSERT INTO config.workstation_setting_type (name, grp, datatype, label) +VALUES ( + 'eg.grid.booking.captured', 'gui', 'object', + oils_i18n_gettext( + 'booking.manage', + 'Grid Config: Booking Captured Reservations', + 'cwst', 'label') +); + + +COMMIT; diff --git a/Open-ILS/src/templates/staff/navbar.tt2 b/Open-ILS/src/templates/staff/navbar.tt2 index 220c3d1d1f..1a90182afb 100644 --- a/Open-ILS/src/templates/staff/navbar.tt2 +++ b/Open-ILS/src/templates/staff/navbar.tt2 @@ -451,7 +451,7 @@
  • - + [% l('Capture Resources') %] diff --git a/docs/RELEASE_NOTES_NEXT/Circulation/angular-booking-capture.adoc b/docs/RELEASE_NOTES_NEXT/Circulation/angular-booking-capture.adoc new file mode 100644 index 0000000000..3d2aca728d --- /dev/null +++ b/docs/RELEASE_NOTES_NEXT/Circulation/angular-booking-capture.adoc @@ -0,0 +1,13 @@ +Booking Capture is now in Angular +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The interface to capture resources for booking +reservations has been re-implemented in Angular. +Other booking screens, such as Pick Up and +Manage Reservations, now include an option to +re-print capture slips. + +System administrators can now edit the template +for booking capture slips in Administration -> +Server administration -> Print templates. +