]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.ts
LP1904036 Checkout receipts uses 'receipt' printer role
[Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / circ / patron / checkout.component.ts
1 import {Component, OnInit, AfterViewInit, Input, ViewChild} from '@angular/core';
2 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
3 import {Subscription, Observable, empty, of, from} from 'rxjs';
4 import {tap, switchMap} from 'rxjs/operators';
5 import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
6 import {IdlObject} from '@eg/core/idl.service';
7 import {OrgService} from '@eg/core/org.service';
8 import {PcrudService} from '@eg/core/pcrud.service';
9 import {NetService} from '@eg/core/net.service';
10 import {PatronService} from '@eg/staff/share/patron/patron.service';
11 import {PatronContextService, CircGridEntry} from './patron.service';
12 import {CheckoutParams, CheckoutResult, CircService
13     } from '@eg/staff/share/circ/circ.service';
14 import {PromptDialogComponent} from '@eg/share/dialog/prompt.component';
15 import {GridDataSource, GridColumn, GridCellTextGenerator} from '@eg/share/grid/grid';
16 import {GridComponent} from '@eg/share/grid/grid.component';
17 import {Pager} from '@eg/share/util/pager';
18 import {StoreService} from '@eg/core/store.service';
19 import {ServerStoreService} from '@eg/core/server-store.service';
20 import {AudioService} from '@eg/share/util/audio.service';
21 import {CopyAlertsDialogComponent
22     } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
23 import {BarcodeSelectComponent
24     } from '@eg/staff/share/barcodes/barcode-select.component';
25 import {ToastService} from '@eg/share/toast/toast.service';
26 import {StringComponent} from '@eg/share/string/string.component';
27 import {AuthService} from '@eg/core/auth.service';
28 import {PrintService} from '@eg/share/print/print.service';
29
30 const SESSION_DUE_DATE = 'eg.circ.checkout.is_until_logout';
31
32 @Component({
33   templateUrl: 'checkout.component.html',
34   selector: 'eg-patron-checkout'
35 })
36 export class CheckoutComponent implements OnInit, AfterViewInit {
37     static autoId = 0;
38
39     maxNoncats = 99; // Matches AngJS version
40     checkoutNoncat: IdlObject = null;
41     checkoutBarcode = '';
42     gridDataSource: GridDataSource = new GridDataSource();
43     cellTextGenerator: GridCellTextGenerator;
44     dueDate: string;
45     dueDateOptions: 0 | 1 | 2 = 0; // auto date; specific date; session date
46     printOnComplete = true;
47     strictBarcode = false;
48
49     private copiesInFlight: {[barcode: string]: boolean} = {};
50
51     @ViewChild('nonCatCount')
52         private nonCatCount: PromptDialogComponent;
53     @ViewChild('checkoutsGrid')
54         private checkoutsGrid: GridComponent;
55     @ViewChild('copyAlertsDialog')
56         private copyAlertsDialog: CopyAlertsDialogComponent;
57     @ViewChild('barcodeSelect')
58         private barcodeSelect: BarcodeSelectComponent;
59     @ViewChild('receiptEmailed')
60         private receiptEmailed: StringComponent;
61
62     constructor(
63         private router: Router,
64         private store: StoreService,
65         private serverStore: ServerStoreService,
66         private org: OrgService,
67         private pcrud: PcrudService,
68         private net: NetService,
69         public circ: CircService,
70         public patronService: PatronService,
71         public context: PatronContextService,
72         private toast: ToastService,
73         private auth: AuthService,
74         private printer: PrintService,
75         private audio: AudioService
76     ) {}
77
78     ngOnInit() {
79         this.circ.getNonCatTypes();
80
81         this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
82             return from(this.context.checkouts);
83         };
84
85         this.cellTextGenerator = {
86             title: row => row.title
87         };
88
89         if (this.store.getSessionItem(SESSION_DUE_DATE)) {
90             this.dueDate = this.store.getSessionItem('eg.circ.checkout.due_date');
91             this.toggleDateOptions(2);
92         }
93
94         this.serverStore.getItem('circ.staff_client.do_not_auto_attempt_print')
95         .then(noPrint => {
96             this.printOnComplete = !(
97                 noPrint &&
98                 noPrint.includes('Checkout')
99             );
100         });
101
102         this.serverStore.getItem('circ.checkout.strict_barcode')
103         .then(strict => this.strictBarcode = strict);
104     }
105
106     ngAfterViewInit() {
107         this.focusInput();
108     }
109
110     focusInput() {
111         const input = document.getElementById('barcode-input');
112         if (input) { input.focus(); }
113     }
114
115     collectParams(): Promise<CheckoutParams> {
116
117         const params: CheckoutParams = {
118             patron_id: this.context.summary.id,
119             _checkbarcode: this.strictBarcode,
120             _worklog: {
121                 user: this.context.summary.patron.family_name(),
122                 patron_id: this.context.summary.id
123             }
124         };
125
126         if (this.checkoutNoncat) {
127
128             return this.noncatPrompt().toPromise().then(count => {
129                 if (!count) { return null; }
130                 params.noncat = true;
131                 params.noncat_count = count;
132                 params.noncat_type = this.checkoutNoncat.id();
133                 return params;
134             });
135
136         } else if (this.checkoutBarcode) {
137
138             if (this.dueDateOptions > 0) { params.due_date = this.dueDate; }
139
140             return this.barcodeSelect.getBarcode('asset', this.checkoutBarcode)
141             .then(selection => {
142                 if (selection) {
143                     params.copy_id = selection.id;
144                     params.copy_barcode = selection.barcode;
145                     return params;
146                 } else {
147                     // User canceled the multi-match selection dialog.
148                     return null;
149                 }
150             });
151         }
152
153         return Promise.resolve(null);
154     }
155
156     checkout(params?: CheckoutParams, override?: boolean): Promise<CheckoutResult> {
157
158         let barcode;
159         const promise = params ? Promise.resolve(params) : this.collectParams();
160
161         return promise.then((collectedParams: CheckoutParams) => {
162             if (!collectedParams) { return null; }
163
164             barcode = collectedParams.copy_barcode || '';
165
166             if (barcode) {
167
168                 if (this.copiesInFlight[barcode]) {
169                     console.debug('Item ' + barcode + ' is already mid-checkout');
170                     return null;
171                 }
172
173                 this.copiesInFlight[barcode] = true;
174             }
175
176             return this.circ.checkout(collectedParams);
177         })
178
179         .then((result: CheckoutResult) => {
180             if (result && result.success) {
181                 this.gridifyResult(result);
182             }
183             delete this.copiesInFlight[barcode];
184             this.resetForm();
185             return result;
186         })
187
188         .finally(() => delete this.copiesInFlight[barcode]);
189     }
190
191     resetForm() {
192         this.checkoutBarcode = '';
193         this.checkoutNoncat = null;
194         this.focusInput();
195     }
196
197     gridifyResult(result: CheckoutResult) {
198         const entry: CircGridEntry = {
199             index: CheckoutComponent.autoId++,
200             copy: result.copy,
201             circ: result.circ,
202             dueDate: null,
203             copyAlertCount: 0,
204             nonCatCount: 0,
205             record: result.record,
206             volume: result.volume,
207             patron: result.patron,
208             title: result.title,
209             author: result.author,
210             isbn: result.isbn
211         };
212
213         if (result.nonCatCirc) {
214
215             entry.title = this.checkoutNoncat.name();
216             entry.dueDate = result.nonCatCirc.duedate();
217             entry.nonCatCount = result.params.noncat_count;
218
219         } else if (result.circ) {
220             entry.dueDate = result.circ.due_date();
221         }
222
223         if (entry.copy) {
224             // Fire and forget this one
225
226             this.pcrud.search('aca',
227                 {copy : entry.copy.id(), ack_time : null}, {}, {atomic: true}
228             ).subscribe(alerts => entry.copyAlertCount = alerts.length);
229         }
230
231         this.context.checkouts.unshift(entry);
232         this.checkoutsGrid.reload();
233
234         // update summary data
235         this.context.refreshPatron();
236     }
237
238     noncatPrompt(): Observable<number> {
239         return this.nonCatCount.open()
240         .pipe(switchMap(count => {
241
242             if (count === null || count === undefined) {
243                 return empty(); // dialog canceled
244             }
245
246             // Even though the prompt has a type and min/max values,
247             // users can still manually enter bogus values.
248             count = Number(count);
249             if (count > 0 && count < this.maxNoncats) {
250                 return of(count);
251             } else {
252                 // Bogus value.  Try again
253                 return this.noncatPrompt();
254             }
255         }));
256     }
257
258     setDueDate(iso: string) {
259         this.dueDate = iso;
260         this.store.setSessionItem('eg.circ.checkout.due_date', this.dueDate);
261     }
262
263
264     // 0: use server due date
265     // 1: use specific due date once
266     // 2: use specific due date until the end of the session.
267     toggleDateOptions(value: 1 | 2) {
268         if (this.dueDateOptions > 0) {
269
270             if (value === 1) { // 1 or 2 -> 0
271                 this.dueDateOptions = 0;
272                 this.store.removeSessionItem(SESSION_DUE_DATE);
273
274             } else if (this.dueDateOptions === 1) { // 1 -> 2
275
276                 this.dueDateOptions = 2;
277                 this.store.setSessionItem(SESSION_DUE_DATE, true);
278
279             } else { // 2 -> 1
280
281                 this.dueDateOptions = 1;
282                 this.store.removeSessionItem(SESSION_DUE_DATE);
283             }
284
285         } else {
286
287             this.dueDateOptions = value;
288             if (value === 2) {
289                 this.store.setSessionItem(SESSION_DUE_DATE, true);
290             }
291         }
292     }
293
294     selectedCopyIds(rows: CircGridEntry[]): number[] {
295         return rows
296             .filter(row => row.copy)
297             .map(row => Number(row.copy.id()));
298     }
299
300     openItemAlerts(rows: CircGridEntry[], mode: string) {
301         const copyIds = this.selectedCopyIds(rows);
302         if (copyIds.length === 0) { return; }
303
304         this.copyAlertsDialog.copyIds = copyIds;
305         this.copyAlertsDialog.mode = mode;
306         this.copyAlertsDialog.open({size: 'lg'}).subscribe(
307             modified => {
308                 if (modified) {
309                     rows.forEach(row => row.copyAlertCount++);
310                     this.checkoutsGrid.reload();
311                 }
312             }
313         );
314     }
315
316     toggleStrictBarcode(active: boolean) {
317         if (active) {
318             this.serverStore.setItem('circ.checkout.strict_barcode', true);
319         } else {
320             this.serverStore.removeItem('circ.checkout.strict_barcode');
321         }
322     }
323
324     patronHasEmail(): boolean {
325         if (!this.context.summary) { return false; }
326         const patron = this.context.summary.patron;
327         return (
328             patron.email() &&
329             patron.email().match(/.*@.*/) !== null
330         );
331     }
332
333     mayEmailReceipt(): boolean {
334         if (!this.context.summary) { return false; }
335         const patron = this.context.summary.patron;
336         const setting = patron.settings()
337             .filter(s => s.name() === 'circ.send_email_checkout_receipts')[0];
338
339         return (
340             this.patronHasEmail() &&
341             setting &&
342             setting.value() === 'true' // JSON encoded
343         );
344     }
345
346     quickReceipt() {
347         if (this.mayEmailReceipt()) {
348             this.emailReceipt();
349         } else {
350             this.printReceipt();
351         }
352     }
353
354     doneAutoReceipt() {
355         if (this.mayEmailReceipt()) {
356             this.emailReceipt(true);
357         } else if (this.printOnComplete) {
358             this.printReceipt(true);
359         }
360     }
361
362     emailReceipt(redirect?: boolean) {
363         if (this.patronHasEmail() && this.context.checkouts.length > 0) {
364             return this.net.request(
365                 'open-ils.circ',
366                 'open-ils.circ.checkout.batch_notify.session.atomic',
367                 this.auth.token(),
368                 this.context.summary.id,
369                 this.context.checkouts.map(c => c.circ.id())
370             ).subscribe(_ => {
371                 this.toast.success(this.receiptEmailed.text);
372                 if (redirect) { this.doneRedirect(); }
373             });
374         }
375     }
376
377     printReceipt(redirect?: boolean) {
378         if (this.context.checkouts.length === 0) { return; }
379
380         if (redirect) {
381             // Wait for the print job to be queued before redirecting
382             const sub: Subscription =
383                 this.printer.printJobQueued$.subscribe(_ => {
384                 sub.unsubscribe();
385                 this.doneRedirect();
386             });
387         }
388
389         this.printer.print({
390             printContext: 'receipt',
391             templateName: 'checkout',
392             contextData: {checkouts: this.context.checkouts}
393         });
394     }
395
396     doneRedirect() {
397         this.router.navigate(['/staff/circ/patron/bcsearch']);
398     }
399
400 }
401