<eg-grid-column path="copy.circ_lib.shortname"
label="Circulation Library" i18n-label></eg-grid-column>
+ <eg-grid-column path="copy.status.name"
+ label="Item Status" i18n-label></eg-grid-column>
+
</eg-grid>
</div>
</div>
<ng-container *ngIf="!r.record">{{r.title}}</ng-container>
</ng-template>
+<ng-template #copyAlertsTemplate let-r="row">
+ <button class="btn btn-outline-dark btn-sm p-1"
+ (click)="openItemAlerts([r], 'manage')" i18n>
+ Manage ({{r.copyAlertCount}})
+ </button>
+</ng-template>
+
<div class="row">
<div class="col-lg-12">
<eg-grid #checkoutsGrid [dataSource]="gridDataSource" [sortable]="true"
<eg-grid-column path="nonCatCount" label="Non-Cataloged Count"
i18n-label></eg-grid-column>
+ <eg-grid-column name="copyAlerts" label="Alerts" i18n-label
+ [cellTemplate]="copyAlertsTemplate"></eg-grid-column>
+
</eg-grid>
</div>
</div>
import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
import {IdlObject} from '@eg/core/idl.service';
import {OrgService} from '@eg/core/org.service';
+import {PcrudService} from '@eg/core/pcrud.service';
import {NetService} from '@eg/core/net.service';
import {PatronService} from '@eg/staff/share/patron/patron.service';
import {PatronContextService, CircGridEntry} from './patron.service';
private store: StoreService,
private serverStore: ServerStoreService,
private org: OrgService,
+ private pcrud: PcrudService,
private net: NetService,
public circ: CircService,
public patronService: PatronService,
copy: result.copy,
circ: result.circ,
dueDate: null,
- copyAlertCount: 0, // TODO
+ copyAlertCount: 0,
nonCatCount: 0,
title: result.title,
author: result.author,
entry.dueDate = result.nonCatCirc.duedate();
entry.nonCatCount = result.params.noncat_count;
- } else {
+ } else if (result.circ) {
+ entry.dueDate = result.circ.due_date();
+ }
- if (result.circ) {
- entry.dueDate = result.circ.due_date();
- }
+ if (entry.copy) {
+ // Fire and forget this one
+
+ this.pcrud.search('aca',
+ {copy : entry.copy.id(), ack_time : null}, {}, {atomic: true}
+ ).subscribe(alerts => entry.copyAlertCount = alerts.length);
}
this.context.checkouts.unshift(entry);
import {CircComponentsComponent} from './components.component';
import {StringService} from '@eg/share/string/string.service';
import {ServerStoreService} from '@eg/core/server-store.service';
+import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
export interface CircDisplayInfo {
title?: string;
dummy_isbn?: string;
circ_modifier?: string;
void_overdues?: boolean;
+ new_copy_alerts?: boolean;
// internal tracking
_override?: boolean;
hold?: IdlObject;
patron?: IdlObject;
transit?: IdlObject;
+ copyAlerts?: IdlObject[];
// Calculated values
title?: string;
void_overdues?: boolean;
auto_print_hold_transits?: boolean;
backdate?: string;
+ capture?: string;
+ next_copy_status?: number[];
+ new_copy_alerts?: boolean;
// internal / local values that are moved from the API request.
_override?: boolean;
private serverStore: ServerStoreService,
private strings: StringService,
private auth: AuthService,
- private bib: BibRecordService,
+ private holdings: HoldingsService,
+ private bib: BibRecordService
) {}
applySettings(): Promise<any> {
checkout(params: CheckoutParams): Promise<CheckoutResult> {
+ params.new_copy_alerts = true;
params._renewal = false;
console.debug('checking out with', params);
renew(params: CheckoutParams): Promise<CheckoutResult> {
+ params.new_copy_alerts = true;
params._renewal = true;
console.debug('renewing out with', params);
}
checkin(params: CheckinParams): Promise<CheckinResult> {
+ params.new_copy_alerts = true;
console.debug('checking in with', params);
result.author = result.record.author();
result.isbn = result.record.isbn();
- } else if (result.copy) {
+ } else if (copy) {
result.title = result.copy.dummy_title();
result.author = result.copy.dummy_author();
result.isbn = result.copy.dummy_isbn();
this.copyLocationCache[loc.id()] = loc;
});
}
+
+ if (typeof copy.status() !== 'object') {
+ promise = promise.then(_ => this.holdings.getCopyStatuses())
+ .then(stats => {
+ const stat =
+ Object.values(stats).filter(s => s.id() === copy.status())[0];
+ if (stat) { copy.status(stat); }
+ });
+ }
}
if (volume) {
const allEvents = Array.isArray(response) ?
response.map(r => this.evt.parse(r)) : [this.evt.parse(response)];
- console.debug('checkin returned', allEvents.map(e => e.textcode));
+ console.debug('checkin events', allEvents.map(e => e.textcode));
+ console.debug('checkin response', response);
const firstEvent = allEvents[0];
const payload = firstEvent.payload;
// Alerts that require a manual override.
if (allEvents.filter(
e => CAN_OVERRIDE_CHECKIN_ALERTS.includes(e.textcode)).length > 0) {
-
- // Should not be necessary, but good to be safe.
- if (params._override) { return Promise.resolve(null); }
-
- return this.showOverrideDialog(result, allEvents, true);
+ return this.handleOverridableCheckinEvents(result);
}
switch (result.firstEvent.textcode) {
}
handleCheckinSuccess(result: CheckinResult): Promise<CheckinResult> {
+ const copy = result.copy;
+
+ if (!copy) { return Promise.resolve(result); }
+
+ const stat = copy.status();
+ const statId = typeof stat === 'object' ? stat.id() : stat;
- switch (result.copy.status()) {
+ switch (statId) {
case 0: /* AVAILABLE */
case 4: /* MISSING */
default:
this.audio.play('success.checkin');
- const stat = result.copy;
console.debug(`Unusual checkin copy status (may have been
- set via copy alert): ${stat.id()} : ${stat.name()}`);
+ set via copy alert): status=${statId}`);
}
return Promise.resolve(result);
}
- handleOverridableCheckinEvents(
- result: CheckinResult, events: EgEvent[]): Promise<CheckinResult> {
+ handleOverridableCheckinEvents(result: CheckinResult): Promise<CheckinResult> {
const params = result.params;
- const firstEvent = events[0];
+ const events = result.allEvents;
+ const firstEvent = result.firstEvent
if (params._override) {
// Should never get here. Just being safe.
return Promise.reject(null);
}
+
+ if (this.suppressCheckinPopups && events.filter(
+ e => !CAN_SUPPRESS_CHECKIN_ALERTS.includes(e.textcode)).length === 0) {
+ // These events are automatically overridden when suppress
+ // popups are in effect.
+ params._override = true;
+ return this.checkin(params);
+ }
+
+ // New-style alerts are reported via COPY_ALERT_MESSAGE and
+ // includes the alerts in the payload as an array.
+ if (firstEvent.textcode === 'COPY_ALERT_MESSAGE'
+ && Array.isArray(firstEvent.payload)) {
+ this.components.copyAlertManager.alerts = firstEvent.payload;
+ this.components.copyAlertManager.mode = 'checkin';
+
+ return this.components.copyAlertManager.open().toPromise()
+ .then(resp => {
+
+ if (!resp) { return result; } // dialog was canceled
+
+ if (resp.nextStatus !== null) {
+ params.next_copy_status = [resp.nextStatus];
+ params.capture = 'nocapture';
+ }
+
+ params._override = true;
+
+ return this.checkin(params);
+ });
+ }
+
+ return this.showOverrideDialog(result, events, true);
}
<eg-string key="staff.circ.events.CHECKOUT_FAILED_GENERIC"
[template]="genericCheckoutFailedTmpl"></eg-string>
+<eg-copy-alert-manager #copyAlertManager></eg-copy-alert-manager>
+
import {OpenCircDialogComponent} from './open-circ-dialog.component';
import {RouteDialogComponent} from './route-dialog.component';
import {CopyInTransitDialogComponent} from './in-transit-dialog.component';
+import {CopyAlertManagerDialogComponent
+ } from '@eg/staff/share/holdings/copy-alert-manager.component';
/* Container component for sub-components used by circulation actions.
*
@ViewChild('circFailedDialog') circFailedDialog: AlertDialogComponent;
@ViewChild('routeDialog') routeDialog: RouteDialogComponent;
@ViewChild('copyInTransitDialog') copyInTransitDialog: CopyInTransitDialogComponent;
+ @ViewChild('copyAlertManager') copyAlertManager: CopyAlertManagerDialogComponent;
@ViewChild('holdShelfStr') holdShelfStr: StringComponent;
@ViewChild('catalogingStr') catalogingStr: StringComponent;
--- /dev/null
+
+
+<eg-string key="staff.holdings.copyalert.CHECKOUT.NORMAL"
+ text="Normal checkin" i18n-text></eg-string>
+<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"
+ text="Item was marked lost and paid for" i18n-text></eg-string>
+<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"
+ text="Item was marked damaged" i18n-text></eg-string>
+<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"
+ text="Item was marked long overdue" i18n-text></eg-string>
+<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"
+ text="Normal checkout" i18n-text></eg-string>
+<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"
+ text="Item was marked lost and paid for" i18n-text></eg-string>
+<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"
+ text="Item was marked damaged" i18n-text></eg-string>
+<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"
+ text="Item was marked long overdue" i18n-text></eg-string>
+<eg-string key="staff.holdings.copyalert.CHECKOUT.CLAIMSNEVERCHECKEDOUT"
+ text="Item was marked claims never checked out" i18n-text></eg-string>
+
+
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <h4 class="modal-title" i18n>Item Alerts</h4>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="close()">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body">
+
+ <div class="row mb-2" *ngFor="let alert of alerts">
+ <div class="col-lg-4">{{alert._event}}</div>
+ <div class="col-lg-6" [ngClass]="{acknowledged: alert._acked}">
+ {{alert._message}}
+ </div>
+ <div class="col-lg-2">
+ <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"
+ *ngIf="mode == 'checkin' && nextStatuses.length > 0; let index = index">
+ <div class="col-lg-4" i18n>Next item status:</div>
+ <div class="col-lg-5">
+ <ng-container *ngIf="nextStatuses.length == 1">
+ {{nextStatuses[0].name()}}
+ </ng-container>
+ <ng-container *ngIf="nextStatuses.length > 1">
+ <select class="form-control" [(ngModel)]="nextStatus">
+ <option [value]="stat.id()" *ngFor="let stat of nextStatuses">
+ {{stat.name()}}
+ </option>
+ </select>
+ </ng-container>
+ </div>
+ </div>
+
+ </div>
+ <div class="modal-footer">
+ <button class="btn btn-success mr-2" (click)="ok()" i18n>OK/Continue</button>
+ <button type="button" class="btn btn-secondary" (click)="close()" i18n>Cancel</button>
+ </div>
+</ng-template>
--- /dev/null
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Observable, throwError, from} from 'rxjs';
+import {concatMap} from 'rxjs/operators';
+import {NetService} from '@eg/core/net.service';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {StringService} from '@eg/share/string/string.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 {HoldingsService} from './holdings.service';
+
+/**
+ * Dialog for managing copy alerts.
+ */
+
+@Component({
+ selector: 'eg-copy-alert-manager',
+ templateUrl: 'copy-alert-manager.component.html',
+ styles: ['.acknowledged {text-decoration: line-through }']
+})
+
+export class CopyAlertManagerDialogComponent
+ extends DialogComponent implements OnInit {
+
+ mode: string;
+ alerts: IdlObject[];
+ nextStatuses: IdlObject[];
+ nextStatus: number;
+
+ constructor(
+ private modal: NgbModal,
+ private toast: ToastService,
+ private net: NetService,
+ private idl: IdlService,
+ private pcrud: PcrudService,
+ private org: OrgService,
+ private auth: AuthService,
+ private strings: StringService,
+ private holdings: HoldingsService
+ ) { super(modal); }
+
+ ngOnInit() {}
+
+ open(ops?: NgbModalOptions): Observable<any> {
+
+ this.nextStatus = null;
+
+ let promise = Promise.resolve(null);
+ this.alerts.forEach(copyAlert =>
+ promise = promise.then(_ => this.ingestAlert(copyAlert)));
+
+ return from(promise).pipe(concatMap(_ => super.open(ops)));
+ }
+
+ ingestAlert(copyAlert: IdlObject): Promise<any> {
+ let promise = Promise.resolve(null);
+
+ const state = copyAlert.alert_type().state();
+ copyAlert._event = copyAlert.alert_type().event();
+
+ if (copyAlert.note()) {
+ copyAlert._message = copyAlert.note();
+ } else {
+ const key = `staff.holdings.copyalert.${copyAlert._event}.${state}`;
+ promise = promise.then(_ => {
+ return this.strings.interpolate(key)
+ .then(str => copyAlert._message = str);
+ });
+ }
+
+ const nextStatuses: number[] = [];
+ this.nextStatuses = [];
+
+ if (copyAlert.temp() === 'f') { return promise; }
+
+ copyAlert.alert_type().next_status().forEach(statId => {
+ if (!nextStatuses.includes(statId)) {
+ nextStatuses.push(statId);
+ }
+ });
+
+ if (this.mode === 'checkin' && nextStatuses.length > 0) {
+
+ promise = promise.then(_ => this.holdings.getCopyStatuses())
+ .then(statMap => {
+ nextStatuses.forEach(statId => {
+ const wanted = statMap[statId];
+ if (wanted) { this.nextStatuses.push(wanted); }
+ })
+
+ if (this.nextStatuses.length > 0) {
+ this.nextStatus = this.nextStatuses[0].id();
+ }
+ });
+ }
+
+ return promise;
+ }
+
+ canBeAcked(copyAlert: IdlObject): boolean {
+ return !copyAlert.ack_time() && copyAlert.temp() === 't';
+ }
+
+ canBeRemoved(copyAlert: IdlObject): boolean {
+ return !copyAlert.ack_time() && copyAlert.temp() === 'f';
+ }
+
+ isAcked(copyAlert: IdlObject): boolean {
+ return copyAlert._acked;
+ }
+
+ ok() {
+ const acks: IdlObject[] = [];
+ this.alerts.forEach(copyAlert => {
+
+ if (copyAlert._acked) {
+ copyAlert.ack_time('now');
+ copyAlert.ack_staff(this.auth.user().id());
+ copyAlert.ischanged(true);
+ acks.push(copyAlert);
+ }
+
+ if (acks.length > 0) {
+ this.pcrud.update(acks).toPromise()
+ .then(_ => this.close({nextStatus: this.nextStatus}));
+ } else {
+ this.close({nextStatus: this.nextStatus});
+ }
+ });
+ }
+}
+
import {TransferItemsComponent} from './transfer-items.component';
import {TransferHoldingsComponent} from './transfer-holdings.component';
import {BatchItemAttrComponent} from './batch-item-attr.component';
+import {CopyAlertManagerDialogComponent} from './copy-alert-manager.component';
@NgModule({
declarations: [
ConjoinedItemsDialogComponent,
TransferItemsComponent,
TransferHoldingsComponent,
- BatchItemAttrComponent
+ BatchItemAttrComponent,
+ CopyAlertManagerDialogComponent
],
imports: [
StaffCommonModule,
ConjoinedItemsDialogComponent,
TransferItemsComponent,
TransferHoldingsComponent,
- BatchItemAttrComponent
+ BatchItemAttrComponent,
+ CopyAlertManagerDialogComponent
],
providers: [
HoldingsService
*/
import {Injectable, EventEmitter} from '@angular/core';
import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {tap} from 'rxjs/operators';
import {NetService} from '@eg/core/net.service';
import {AnonCacheService} from '@eg/share/util/anon-cache.service';
import {PcrudService} from '@eg/core/pcrud.service';
@Injectable()
export class HoldingsService {
+ copyStatuses: {[id: number]: IdlObject};
+
constructor(
private net: NetService,
private auth: AuthService,
18 // Canceled Transit
]);
}
+
+ getCopyStatuses(): Promise<{[id: number]: IdlObject}> {
+ if (this.copyStatuses) {
+ return Promise.resolve(this.copyStatuses);
+ }
+
+ this.copyStatuses = {};
+ return this.pcrud.retrieveAll('ccs', {order_by: {ccs: 'name'}})
+ .pipe(tap(stat => this.copyStatuses[stat.id()] = stat))
+ .toPromise().then(_ => this.copyStatuses);
+ }
}