LP1904036 Checkout/renew receipts
authorBill Erickson <berickxx@gmail.com>
Tue, 11 May 2021 15:31:47 +0000 (11:31 -0400)
committerGalen Charlton <gmc@equinoxOLI.org>
Fri, 28 Oct 2022 00:13:34 +0000 (20:13 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Jane Sandberg <js7389@princeton.edu>
Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.html
Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.ts
Open-ILS/src/eg2/src/app/staff/circ/renew/renew.component.ts
Open-ILS/src/eg2/src/app/staff/share/holdings/copy-alert-manager.component.html
Open-ILS/src/eg2/src/app/staff/share/holdings/copy-alert-manager.component.ts
Open-ILS/src/eg2/src/app/staff/share/worklog/worklog.service.ts
Open-ILS/src/sql/Pg/upgrade/XXXX.data.angular-patron.sql

index e9a1e88..7d48b78 100644 (file)
@@ -2,6 +2,8 @@
 <eg-progress-dialog #progressDialog></eg-progress-dialog>
 <eg-barcode-select #barcodeSelect></eg-barcode-select>
 <eg-copy-alerts-dialog #copyAlertsDialog></eg-copy-alerts-dialog>
+<eg-string #receiptEmailed i18n-text text="Receipt Successfully Emailed">
+</eg-string>
 
 <eg-prompt-dialog #nonCatCount
   promptType="number"
   </div>
 </div>
 
+<div class="row mt-3 pt-3">
+  <div class="col-lg-12 d-flex">
+    <div class="flex-1"></div>
+    <div class="mr-3">
+      <div class="form-check form-check-inline">
+        <input class="form-check-input" type="checkbox" 
+          (ngModelChange)="toggleStrictBarcode($event)"
+          id="strict-barcode-cbox" [(ngModel)]="strictBarcode"/>
+        <label class="form-check-label" 
+          for="strict-barcode-cbox" i18n>Strict Barcode</label>
+      </div>
+    </div>
+    <div class="mr-3">
+      <div class="input-group">
+        <button class="btn btn-outline-dark" (click)="quickReceipt()" i18n>
+          Quick Receipt
+        </button>
+        <div class="input-group-append">
+          <div ngbDropdown>
+            <button ngbDropdownToggle class="btn btn-outline-dark">
+            </button>
+            <div ngbDropdownMenu>
+              <button ngbDropdownItem (click)="emailReceipt()" 
+                [disabled]="!mayEmailReceipt()" i18n>Email Receipt</button>
+              <button ngbDropdownItem (click)="printReceipt()" i18n>Print Receipt</button>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="mr-3">
+      <div class="input-group">
+        <button class="btn btn-outline-dark" (click)="doneAutoReceipt()" i18n>
+          Done
+        </button>
+        <div class="input-group-append">
+          <div ngbDropdown>
+            <button ngbDropdownToggle class="btn btn-outline-dark">
+            </button>
+            <div ngbDropdownMenu>
+              <button ngbDropdownItem (click)="emailReceipt(true)" 
+                [disabled]="!mayEmailReceipt()" i18n>Email Receipt</button>
+              <button ngbDropdownItem 
+                (click)="printReceipt(true)" i18n>Print Receipt</button>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+
 
index ee90e0c..fc4a46a 100644 (file)
@@ -1,6 +1,6 @@
 import {Component, OnInit, AfterViewInit, Input, ViewChild} from '@angular/core';
 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
-import {Observable, empty, of, from} from 'rxjs';
+import {Subscription, Observable, empty, of, from} from 'rxjs';
 import {tap, switchMap} from 'rxjs/operators';
 import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
 import {IdlObject} from '@eg/core/idl.service';
@@ -22,6 +22,10 @@ import {CopyAlertsDialogComponent
     } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
 import {BarcodeSelectComponent
     } from '@eg/staff/share/barcodes/barcode-select.component';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {AuthService} from '@eg/core/auth.service';
+import {PrintService} from '@eg/share/print/print.service';
 
 const SESSION_DUE_DATE = 'eg.circ.checkout.is_until_logout';
 
@@ -38,6 +42,7 @@ export class CheckoutComponent implements OnInit, AfterViewInit {
     cellTextGenerator: GridCellTextGenerator;
     dueDate: string;
     dueDateOptions: 0 | 1 | 2 = 0; // auto date; specific date; session date
+    printOnComplete = true;
 
     private copiesInFlight: {[barcode: string]: boolean} = {};
 
@@ -49,8 +54,11 @@ export class CheckoutComponent implements OnInit, AfterViewInit {
         private copyAlertsDialog: CopyAlertsDialogComponent;
     @ViewChild('barcodeSelect')
         private barcodeSelect: BarcodeSelectComponent;
+    @ViewChild('receiptEmailed')
+        private receiptEmailed: StringComponent;
 
     constructor(
+        private router: Router,
         private store: StoreService,
         private serverStore: ServerStoreService,
         private org: OrgService,
@@ -59,6 +67,9 @@ export class CheckoutComponent implements OnInit, AfterViewInit {
         public circ: CircService,
         public patronService: PatronService,
         public context: PatronContextService,
+        private toast: ToastService,
+        private auth: AuthService,
+        private printer: PrintService,
         private audio: AudioService
     ) {}
 
@@ -77,6 +88,14 @@ export class CheckoutComponent implements OnInit, AfterViewInit {
             this.dueDate = this.store.getSessionItem('eg.circ.checkout.due_date');
             this.toggleDateOptions(2);
         }
+
+        this.serverStore.getItem('circ.staff_client.do_not_auto_attempt_print')
+        .then(noPrint => {
+            this.printOnComplete = !(
+                noPrint &&
+                noPrint.includes('Checkout')
+            );
+        });
     }
 
     ngAfterViewInit() {
@@ -290,5 +309,90 @@ export class CheckoutComponent implements OnInit, AfterViewInit {
             }
         );
     }
+
+    toggleStrictBarcode(active: boolean) {
+        if (active) {
+            this.serverStore.setItem('circ.checkout.strict_barcode', true);
+        } else {
+            this.serverStore.removeItem('circ.checkout.strict_barcode');
+        }
+    }
+
+    patronHasEmail(): boolean {
+        if (!this.context.summary) { return false; }
+        const patron = this.context.summary.patron;
+        return (
+            patron.email() &&
+            patron.email().match(/.*@.*/) !== null
+        );
+    }
+
+    mayEmailReceipt(): boolean {
+        if (!this.context.summary) { return false; }
+        const patron = this.context.summary.patron;
+        const setting = patron.settings()
+            .filter(s => s.name() === 'circ.send_email_checkout_receipts')[0];
+
+        return (
+            this.patronHasEmail() &&
+            setting &&
+            setting.value() === 'true' // JSON encoded
+        );
+    }
+
+    quickReceipt() {
+        if (this.mayEmailReceipt()) {
+            this.emailReceipt();
+        } else {
+            this.printReceipt();
+        }
+    }
+
+    doneAutoReceipt() {
+        if (this.mayEmailReceipt()) {
+            this.emailReceipt(true);
+        } else if (this.printOnComplete) {
+            this.printReceipt(true);
+        }
+    }
+
+    emailReceipt(redirect?: boolean) {
+        if (this.patronHasEmail() && this.context.checkouts.length > 0) {
+            return this.net.request(
+                'open-ils.circ',
+                'open-ils.circ.checkout.batch_notify.session.atomic',
+                this.auth.token(),
+                this.context.summary.id,
+                this.context.checkouts.map(c => c.circ.id())
+            ).subscribe(_ => {
+                this.toast.success(this.receiptEmailed.text);
+                if (redirect) { this.doneRedirect(); }
+            });
+        }
+    }
+
+    printReceipt(redirect?: boolean) {
+        if (this.context.checkouts.length === 0) { return; }
+
+        if (redirect) {
+            // Wait for the print job to be queued before redirecting
+            const sub: Subscription =
+                this.printer.printJobQueued$.subscribe(_ => {
+                sub.unsubscribe();
+                this.doneRedirect();
+            });
+        }
+
+        this.printer.print({
+            printContext: 'default',
+            templateName: 'checkout',
+            contextData: {checkouts: this.context.checkouts}
+        });
+    }
+
+    doneRedirect() {
+        this.router.navigate(['/staff/circ/patron/bcsearch']);
+    }
+
 }
 
index ea40697..7cd30ea 100644 (file)
@@ -181,9 +181,9 @@ export class RenewComponent implements OnInit, AfterViewInit {
 
     toggleStrictBarcode(active: boolean) {
         if (active) {
-            this.store.setItem('circ.checkin.strict_barcode', true);
+            this.store.setItem('circ.renew.strict_barcode', true);
         } else {
-            this.store.removeItem('circ.checkin.strict_barcode');
+            this.store.removeItem('circ.renew.strict_barcode');
         }
     }
 
index 8bf3be0..99b51ee 100644 (file)
@@ -1,39 +1,57 @@
 
 
-<eg-string key="staff.holdings.copyalert.CHECKOUT.NORMAL" 
+<eg-string key="staff.holdings.copyalert.CHECKOUT.NORMAL"
   text="Normal checkin" i18n-text></eg-string>
-<eg-string key="staff.holdings.copyalert.CHECKOUT.LOST" 
+<eg-string key="staff.holdings.copyalert.CHECKOUT.LOST"
   text="Item was marked lost" i18n-text></eg-string>
-<eg-string key="staff.holdings.copyalert.CHECKOUT.LOST_AND_PAID" 
+<eg-string key="staff.holdings.copyalert.CHECKOUT.LOST_AND_PAID"
   text="Item was marked lost and paid for" i18n-text></eg-string>
-<eg-string key="staff.holdings.copyalert.CHECKOUT.MISSING" 
+<eg-string key="staff.holdings.copyalert.CHECKOUT.MISSING"
   text="Item was marked missing" i18n-text></eg-string>
-<eg-string key="staff.holdings.copyalert.CHECKOUT.DAMAGED" 
+<eg-string key="staff.holdings.copyalert.CHECKOUT.DAMAGED"
   text="Item was marked damaged" i18n-text></eg-string>
-<eg-string key="staff.holdings.copyalert.CHECKOUT.CLAIMSRETURNED" 
+<eg-string key="staff.holdings.copyalert.CHECKOUT.CLAIMSRETURNED"
   text="Item was marked claims returned" i18n-text></eg-string>
-<eg-string key="staff.holdings.copyalert.CHECKOUT.LONGOVERDUE" 
+<eg-string key="staff.holdings.copyalert.CHECKOUT.LONGOVERDUE"
   text="Item was marked long overdue" i18n-text></eg-string>
-<eg-string key="staff.holdings.copyalert.CHECKOUT.CLAIMSNEVERCHECKEDOUT" 
+<eg-string key="staff.holdings.copyalert.CHECKOUT.CLAIMSNEVERCHECKEDOUT"
   text="Item was marked claims never checked out" i18n-text></eg-string>
-<eg-string key="staff.holdings.copyalert.CHECKOUT.NORMAL" 
+<eg-string key="staff.holdings.copyalert.CHECKOUT.NORMAL"
   text="Normal checkout" i18n-text></eg-string>
-<eg-string key="staff.holdings.copyalert.CHECKOUT.LOST" 
+<eg-string key="staff.holdings.copyalert.CHECKOUT.LOST"
   text="Item was marked lost" i18n-text></eg-string>
-<eg-string key="staff.holdings.copyalert.CHECKOUT.LOST_AND_PAID" 
+<eg-string key="staff.holdings.copyalert.CHECKOUT.LOST_AND_PAID"
   text="Item was marked lost and paid for" i18n-text></eg-string>
-<eg-string key="staff.holdings.copyalert.CHECKOUT.MISSING" 
+<eg-string key="staff.holdings.copyalert.CHECKOUT.MISSING"
   text="Item was marked missing" i18n-text></eg-string>
-<eg-string key="staff.holdings.copyalert.CHECKOUT.DAMAGED" 
+<eg-string key="staff.holdings.copyalert.CHECKOUT.DAMAGED"
   text="Item was marked damaged" i18n-text></eg-string>
-<eg-string key="staff.holdings.copyalert.CHECKOUT.CLAIMSRETURNED" 
+<eg-string key="staff.holdings.copyalert.CHECKOUT.CLAIMSRETURNED"
   text="Item was marked claims returned" i18n-text></eg-string>
-<eg-string key="staff.holdings.copyalert.CHECKOUT.LONGOVERDUE" 
+<eg-string key="staff.holdings.copyalert.CHECKOUT.LONGOVERDUE"
   text="Item was marked long overdue" i18n-text></eg-string>
-<eg-string key="staff.holdings.copyalert.CHECKOUT.CLAIMSNEVERCHECKEDOUT" 
+<eg-string key="staff.holdings.copyalert.CHECKOUT.CLAIMSNEVERCHECKEDOUT"
   text="Item was marked claims never checked out" i18n-text></eg-string>
 
 
+<eg-string key="staff.holdings.copyalert.CHECKIN.NORMAL"
+  i18n-text text="Normal checkout"></eg-string>
+<eg-string key="staff.holdings.copyalert.CHECKIN.LOST"
+  i18n-text text="Item was marked lost"></eg-string>
+<eg-string key="staff.holdings.copyalert.CHECKIN.LOST_AND_PAID"
+  i18n-text text="Item was marked lost and paid for"></eg-string>
+<eg-string key="staff.holdings.copyalert.CHECKIN.MISSING"
+  i18n-text text="Item was marked missing"></eg-string>
+<eg-string key="staff.holdings.copyalert.CHECKIN.DAMAGED"
+  i18n-text text="Item was marked damaged"></eg-string>
+<eg-string key="staff.holdings.copyalert.CHECKIN.CLAIMSRETURNED"
+  i18n-text text="Item was marked claims returned"></eg-string>
+<eg-string key="staff.holdings.copyalert.CHECKIN.LONGOVERDUE"
+  i18n-text text="Item was marked long overdue"></eg-string>
+<eg-string key="staff.holdings.copyalert.CHECKIN.CLAIMSNEVERCHECKEDOUT"
+  i18n-text text="Item was marked claims never checked out"></eg-string>
+
+
 <ng-template #dialogContent>
   <div class="modal-header bg-info">
     <h4 class="modal-title" i18n>Item Alerts</h4>
         {{alert._message}}
       </div>
       <div class="col-lg-2">
-        <button class="btn btn-sm btn-outline-dark mr-2" *ngIf="canBeAcked(alert)" 
+        <button class="btn btn-sm btn-outline-dark mr-2" *ngIf="canBeAcked(alert)"
           (click)="alert._acked = !alert._acked" i18n>Clear</button>
       </div>
     </div>
 
-    <div class="row border-top mt-3 pt-3" 
+    <div class="row border-top mt-3 pt-3"
       *ngIf="mode == 'checkin' && nextStatuses.length > 0; let index = index">
       <div class="col-lg-4" i18n>Next item status:</div>
       <div class="col-lg-5">
index 5288753..df0698e 100644 (file)
@@ -79,11 +79,13 @@ export class CopyAlertManagerDialogComponent
 
         if (copyAlert.temp() === 'f') { return promise; }
 
-        copyAlert.alert_type().next_status().forEach(statId => {
-            if (!nextStatuses.includes(statId)) {
-                nextStatuses.push(statId);
-            }
-        });
+        if (copyAlert.alert_type().next_status()) {
+            copyAlert.alert_type().next_status().forEach(statId => {
+                if (!nextStatuses.includes(statId)) {
+                    nextStatuses.push(statId);
+                }
+            });
+        }
 
         if (this.mode === 'checkin' && nextStatuses.length > 0) {
 
index af0818b..8fe3cc1 100644 (file)
@@ -44,7 +44,6 @@ export class WorkLogService {
 
     record(entry: WorkLogEntry) {
 
-console.log('1');
         if (this.maxEntries === null) {
             throw new Error('WorkLogService.loadSettings() required');
             return;
@@ -55,35 +54,27 @@ console.log('1');
                 'Add <eg-worklog-strings-components/> to your component for worklog support');
             return;
         }
-console.log('1');
 
         entry.when = new Date();
         entry.actor = this.auth.user().usrname();
-        console.log(`worklog_${entry.action}`);
-        console.log(this.workLogStrings[`worklog_${entry.action}`]);
         entry.msg = this.workLogStrings[`worklog_${entry.action}`].text;
-console.log('1');
 
         const workLog: WorkLogEntry[] =
             this.store.getLocalItem('eg.work_log') || [];
 
         let patronLog: WorkLogEntry[] =
             this.store.getLocalItem('eg.patron_log') || [];
-console.log('1');
 
         workLog.push(entry);
         if (workLog.length > this.maxEntries) {
             workLog.shift();
         }
-console.log('1');
 
         this.store.setLocalItem('eg.work_log', workLog);
-console.log('1');
 
         if (entry.patron_id) {
             // Remove existing entries that match this patron
             patronLog = patronLog.filter(e => e.patron_id !== entry.patron_id);
-console.log('1');
 
             patronLog.push(entry);
             if (patronLog.length > this.maxPatrons) {
index 09919de..e22f88f 100644 (file)
@@ -1,6 +1,8 @@
 
 BEGIN;
 
+/*
+
 -- SELECT evergreen.upgrade_deps_block_check('TODO', :eg_version); 
 
 -- insert then update for easier iterative development tweaks
@@ -563,6 +565,86 @@ UPDATE config.print_template SET template = $TEMPLATE$
 </div>
 $TEMPLATE$ WHERE name = 'bills_historical';
 
+*/
+
+INSERT INTO config.print_template 
+    (name, label, owner, active, locale, content_type, template)
+VALUES ('checkout', 'Checkout', 1, TRUE, 'en-US', 'text/html', '');
+
+UPDATE config.print_template SET template = $TEMPLATE$
+[% 
+  USE date;
+  USE money = format('$%.2f');
+  SET checkouts = template_data.checkouts;
+%] 
+
+<div>
+  <div>Welcome to [% staff_org.name %]</div>
+  <div>You checked out the following items:</div>
+  <hr/>
+  <ol>
+       [% FOR checkout IN checkouts %]
+    <li>
+      <div>[% checkout.title %]</div>
+      <span>Barcode: </span>
+      <span>[% checkout.copy.barcode %]</span>
+      <span>Call Number: </span>
+      <span>
+      [% IF checkout.volume %]
+           [% volume.prefix.label %] [% volume.label %] [% volume.suffix.label %]
+      [% ELSE %]
+        Not Cataloged
+      [% END %]
+      </span>
+    </li>
+  [% END %]
+  </ol>
+  <hr/>
+  <div>Slip Date: [% date.format(date.now, '%x %r') %]</div>
+  <div>Printed by [% staff.first_given_name %] at [% staff_org.shortname %]</div>
+</div>
+
+$TEMPLATE$ WHERE name = 'checkout';
+
+INSERT INTO config.print_template 
+    (name, label, owner, active, locale, content_type, template)
+VALUES ('renew', 'renew', 1, TRUE, 'en-US', 'text/html', '');
+
+UPDATE config.print_template SET template = $TEMPLATE$
+[% 
+  USE date;
+  USE money = format('$%.2f');
+  SET renewals = template_data.renewals;
+%] 
+
+<div>
+  <div>Welcome to [% staff_org.name %]</div>
+  <div>You renewed the following items:</div>
+  <hr/>
+  <ol>
+       [% FOR renewal IN renewals %]
+    <li>
+      <div>[% renewal.title %]</div>
+      <span>Barcode: </span>
+      <span>[% renewal.copy.barcode %]</span>
+      <span>Call Number: </span>
+      <span>
+      [% IF renewal.volume %]
+           [% volume.prefix.label %] [% volume.label %] [% volume.suffix.label %]
+      [% ELSE %]
+        Not Cataloged
+      [% END %]
+      </span>
+    </li>
+  [% END %]
+  </ol>
+  <hr/>
+  <div>Slip Date: [% date.format(date.now, '%x %r') %]</div>
+  <div>Printed by [% staff.first_given_name %] at [% staff_org.shortname %]</div>
+</div>
+
+$TEMPLATE$ WHERE name = 'renew';
+
 COMMIT;