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';
],
declarations: [
CancelReservationDialogComponent,
+ CaptureComponent,
CreateReservationComponent,
CreateReservationDialogComponent,
ManageReservationsComponent,
--- /dev/null
+<eg-string #noResourceString i18n-text text="No resource found with this barcode"></eg-string>
+<eg-string #captureSuccessString i18n-text text="Resource successfully captured"></eg-string>
+<eg-string #captureFailureString i18n-text text="Could not capture this resource"></eg-string>
+
+<eg-staff-banner bannerText="Booking Capture" i18n-bannerText>
+</eg-staff-banner>
+<eg-title i18n-prefix i18n-suffix prefix="Booking" suffix="Capture"></eg-title>
+
+<form [formGroup]="findResource" class="row">
+ <div class="col-md-4">
+ <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" formControlName="resourceBarcode">
+ </div>
+ </div>
+ </div>
+</form>
+
+<h2 class="text-center mt-2" i18n>Captured today</h2>
+<eg-reservations-grid #capturedTodayGrid status="capturedToday" persistSuffix="captured"></eg-reservations-grid>
+
--- /dev/null
+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();
+ });
+ }
+
+}
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,
) {
}
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<CaptureInformation> => {
+ return this.fetchDataForCaptureSlip$(ids)
+ .pipe(tap((data) => this.printCaptureSlip(data)));
+ }
+
viewItemStatus = (barcode: string) => {
this.pcrud.search('acp', { 'barcode': barcode }, { limit: 1 })
.subscribe((acp) => {
return (new Set(ids).size !== 1);
}
+ private fetchDataForCaptureSlip$ = (ids: number[]): Observable<CaptureInformation> => {
+ 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<CaptureInformation> => {
+ 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$;
+ }
+
}
persistKey="booking.{{persistSuffix}}" >
<eg-grid-toolbar-action label="Edit Selected" i18n-label (onClick)="editSelected($event)" [disableOnRows]="editNotAppropriate"></eg-grid-toolbar-action>
<eg-grid-toolbar-action label="Cancel Selected" i18n-label (onClick)="cancelSelected($event)" [disableOnRows]="cancelNotAppropriate"></eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Reprint Capture Slip" i18n-label (onClick)="reprintCaptureSlip($event)" [disableOnRows]="reprintNotAppropriate"></eg-grid-toolbar-action>
<eg-grid-toolbar-action label="Pick Up Selected" i18n-label (onClick)="pickupSelected($event)" [disableOnRows]="pickupNotAppropriate"></eg-grid-toolbar-action>
<eg-grid-toolbar-action label="Return Selected" i18n-label (onClick)="returnSelected($event)" [disableOnRows]="returnNotAppropriate"></eg-grid-toolbar-action>
<eg-grid-toolbar-action label="View Patron Record" i18n-label (onClick)="viewPatronRecord($event)" [disableOnRows]="notOnePatronSelected"></eg-grid-toolbar-action>
@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;
editSelected: (rows: IdlObject[]) => void;
pickupSelected: (rows: IdlObject[]) => void;
pickupResource: (rows: IdlObject) => Observable<any>;
+ reprintCaptureSlip: (rows: IdlObject[]) => void;
returnSelected: (rows: IdlObject[]) => void;
returnResource: (rows: IdlObject) => Observable<any>;
cancelSelected: (rows: IdlObject[]) => void;
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;
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;
};
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 => {
return false;
};
- this.reloadGrid = () => { this.grid.reload(); };
-
this.pickupSelected = (reservations: IdlObject[]) => {
const pickupOne = (thing: IdlObject) => {
if (!thing) { return; }
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',
ngOnChanges() { this.reloadGrid(); }
+ reloadGrid() { this.grid.reload(); }
+
enrichRow$ = (row: IdlObject): Observable<IdlObject> => {
return from(this.org.settings('lib.timezone', row.pickup_lib().id())).pipe(
switchMap((tz) => {
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';
{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},
<span class="material-icons">list</span>
<span i18n>Pull List</span>
</a>
- <a class="dropdown-item" href="/eg/staff/booking/legacy/booking/capture">
+ <a class="dropdown-item" routerLink="/staff/booking/capture">
<span class="material-icons">pin_drop</span>
<span i18n>Capture Resources</span>
</a>
$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.
+-%]
+<div>
+ [% IF data.transit %]
+ <div>This item need to be routed to <strong>data.transit.dest</strong></div>
+ [% ELSE %]
+ <div>This item need to be routed to <strong>RESERVATION SHELF:</strong></div>
+ [% END %]
+ <div>Barcode: [% data.reservation.current_resource.barcode %]</div>
+ <div>Title: [% data.reservation.current_resource.type.name %]</div>
+ <div>Note: [% data.reservation.note %]</div>
+ <br/>
+ <p><strong>Reserved for patron</strong> [% data.reservation.usr.family_name %], [% data.reservation.usr.first_given_name %] [% data.reservation.usr.second_given_name %]
+ <br/>Barcode: [% data.reservation.usr.card.barcode %]</p>
+ <p>Request time: [% date.format(helpers.format_date(data.reservation.request_time, client_timezone), '%x %r', locale) %]
+ <br/>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) %]</p>
+ <p>Slip date: [% date.format(helpers.current_date(client_timezone), '%x %r', locale) %]<br/>
+ Printed by [% data.staff.family_name %], [% data.staff.first_given_name %] [% data.staff.second_given_name %]
+ at [% data.workstation %]</p>
+</div>
+<br/>
+
+$TEMPLATE$
+);
-- Allow for 1k stock templates
SELECT SETVAL('config.print_template_id_seq'::TEXT, 1000);
'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',
--- /dev/null
+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.
+-%]
+<div>
+ [% IF data.transit %]
+ <div>This item need to be routed to <strong>data.transit.dest</strong></div>
+ [% ELSE %]
+ <div>This item need to be routed to <strong>RESERVATION SHELF:</strong></div>
+ [% END %]
+ <div>Barcode: [% data.reservation.current_resource.barcode %]</div>
+ <div>Title: [% data.reservation.current_resource.type.name %]</div>
+ <div>Note: [% data.reservation.note %]</div>
+ <br/>
+ <p><strong>Reserved for patron</strong> [% data.reservation.usr.family_name %], [% data.reservation.usr.first_given_name %] [% data.reservation.usr.second_given_name %]
+ <br/>Barcode: [% data.reservation.usr.card.barcode %]</p>
+ <p>Request time: [% date.format(helpers.format_date(data.reservation.request_time, client_timezone), '%x %r', locale) %]
+ <br/>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) %]</p>
+ <p>Slip date: [% date.format(helpers.current_date(client_timezone), '%x %r', locale) %]<br/>
+ Printed by [% data.staff.family_name %], [% data.staff.first_given_name %] [% data.staff.second_given_name %]
+ at [% data.workstation %]</p>
+</div>
+<br/>
+
+$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;
</a>
</li>
<li>
- <a href="./booking/legacy/booking/capture" target="_self">
+ <a href="/eg2/booking/capture" target="_self">
<span class="glyphicon glyphicon-pushpin"></span>
[% l('Capture Resources') %]
</a>
--- /dev/null
+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.
+