1 import {Component, Input, ViewChild} from '@angular/core';
2 import {Observable, throwError, from} from 'rxjs';
3 import {switchMap} from 'rxjs/operators';
4 import {IdlService, IdlObject} from '@eg/core/idl.service';
5 import {ToastService} from '@eg/share/toast/toast.service';
6 import {AuthService} from '@eg/core/auth.service';
7 import {PcrudService} from '@eg/core/pcrud.service';
8 import {OrgService} from '@eg/core/org.service';
9 import {StringComponent} from '@eg/share/string/string.component';
10 import {DialogComponent} from '@eg/share/dialog/dialog.component';
11 import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
12 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
13 import {ServerStoreService} from '@eg/core/server-store.service';
16 * Dialog for managing copy alerts.
19 export interface CopyAlertsChanges {
20 newAlerts: IdlObject[];
21 changedAlerts: IdlObject[];
25 selector: 'eg-copy-alerts-dialog',
26 templateUrl: 'copy-alerts-dialog.component.html'
29 export class CopyAlertsDialogComponent
30 extends DialogComponent {
32 @Input() copyIds: number[] = [];
34 alertIdMap: { [key: number]: any };
36 mode: string; // create | manage
38 // If true, no attempt is made to save new alerts to the
39 // database. It's assumed this takes place in the calling code.
40 @Input() inPlaceCreateMode = false;
42 // This will not contain "real" alerts, but a deduped set of alert
43 // proxies in a batch context that match on alert_type, temp, note,
44 // and null/not-null for ack_time.
45 alertsInCommon: IdlObject[] = [];
47 alertTypes: ComboboxEntry[];
49 newAlerts: IdlObject[];
52 defaultAlertType = 1; // default default :-)
54 @ViewChild('successMsg', { static: true }) private successMsg: StringComponent;
55 @ViewChild('errorMsg', { static: true }) private errorMsg: StringComponent;
58 private modal: NgbModal, // required for passing to parent
59 private toast: ToastService,
60 private idl: IdlService,
61 private pcrud: PcrudService,
62 private org: OrgService,
63 private auth: AuthService,
64 private serverStore: ServerStoreService) {
65 super(modal); // required for subclassing
71 prepNewAlert(): IdlObject {
72 const newAlert = this.idl.create('aca');
73 newAlert.alert_type(this.defaultAlertType);
74 newAlert.create_staff(this.auth.user().id());
79 return this.copies.length > 0;
83 return this.copies.length > 1;
87 * Fetch the item/record, then open the dialog.
88 * Dialog promise resolves with true/false indicating whether
89 * the mark-damanged action occured or was dismissed.
91 open(args: NgbModalOptions): Observable<CopyAlertsChanges> {
94 this.newAlert = this.prepNewAlert();
96 if (this.copyIds.length === 0 && !this.inPlaceCreateMode) {
97 return throwError('copy ID required');
100 // We're removing the distinction between 'manage' and 'create'
101 // modes and are implementing batch edit for existing alerts
102 // that match on alert_type, temp, note, and whether ack_time
103 // is set or not set.
104 this.mode = 'manage';
106 // Observerify data loading
109 .then(_ => this.getCopies())
110 .then(_ => this.getCopyAlerts())
111 .then(_ => this.getDefaultAlertType())
112 .then(_ => { if (this.defaultAlertType) { this.newAlert.alert_type(this.defaultAlertType); } })
115 // Return open() observable to caller
116 return obs.pipe(switchMap(_ => super.open(args)));
119 getAlertTypes(): Promise<any> {
120 if (this.alertTypes) { return Promise.resolve(); }
122 return this.pcrud.retrieveAll('ccat',
124 scope_org: this.org.ancestors(this.auth.user().ws_ou(), true)
126 ).toPromise().then(alerts => {
127 this.alertTypes = alerts.map(a => ({id: a.id(), label: a.name()}));
131 getCopies(): Promise<any> {
133 // Avoid fetch if we're only adding notes to isnew copies.
134 const ids = this.copyIds.filter(id => id > 0);
135 if (ids.length === 0) { return Promise.resolve(); }
137 return this.pcrud.search('acp', {id: this.copyIds}, {}, {atomic: true})
138 .toPromise().then(copies => {
139 this.copies = copies;
140 copies.forEach(c => c.copy_alerts([]));
144 // Copy alerts for the selected copies which have not been
145 // acknowledged by staff and are within org unit range of
147 getCopyAlerts(): Promise<any> {
148 const typeIds = this.alertTypes.map(a => a.id);
150 return this.pcrud.search('aca',
151 {copy: this.copyIds, ack_time: null, alert_type: typeIds},
153 .toPromise().then(alerts => {
154 alerts.forEach(a => {
155 const copy = this.copies.filter(c => c.id() === a.copy())[0];
156 this.alertIdMap[a.id()] = a;
157 copy.copy_alerts().push(a);
159 if (this.inBatch()) {
160 let potentialMatches = this.copies[0].copy_alerts();
162 this.copies.slice(1).forEach(copy => {
163 potentialMatches = potentialMatches.filter(alertFromFirstCopy =>
164 copy.copy_alerts().some(alertFromCurrentCopy =>
165 this.compositeMatch(alertFromFirstCopy, alertFromCurrentCopy)
170 // potentialMatches now contains alerts that have a "match" in every copy
171 this.alertsInCommon = potentialMatches.map( match => this.cloneAlertForBatchProxy(match) );
176 getDefaultAlertType(): Promise<any> {
177 // TODO fetching the default item alert type from holdings editor
178 // defaults had previously been handled via methods from
179 // VolCopyService. However, as described in LP#2044051, this
180 // caused significant issues with dependency injection.
181 // Consequently, some refactoring may be in order so that
182 // such default values can be managed via a more self-contained
184 return this.serverStore.getItem('eg.cat.volcopy.defaults').then(
186 console.log('eg.cat.volcopy.defaults',defaults);
187 if (defaults?.values?.item_alert_type) {
188 console.log('eg.cat.volcopy.defaults, got here for item_alert_type',defaults.values.item_alert_type);
189 this.defaultAlertType = defaults.values.item_alert_type;
195 getAlertTypeLabel(alert: IdlObject): string {
196 const alertType = this.alertTypes.filter(t => t.id === alert.alert_type());
197 return alertType[0].label;
200 removeAlert(alert: IdlObject) {
201 // the only type of alerts we can remove are pending ones that
202 // we have created during the lifetime of this modal; alerts
203 // that already exist can only be cleared
204 this.newAlerts = this.newAlerts.filter(t => t.id() !== alert.id());
207 // Add the in-progress new note to all copies.
210 this.newAlert.id(this.autoId--);
211 this.newAlert.isnew(true);
212 this.newAlerts.push(this.newAlert);
214 this.newAlert = this.prepNewAlert();
218 compositeMatch(a: IdlObject, b: IdlObject): boolean {
219 return a.alert_type() === b.alert_type()
220 && a.temp() === b.temp()
221 && a.note() === b.note()
223 (a.ack_time() === null && b.ack_time() === null)
224 || (a.ack_time() !== null && b.ack_time() !== null)
228 setAlert(target: IdlObject, source: IdlObject) {
229 target.ack_staff(source.ack_staff());
230 if (source.ack_time() === 'now') {
231 target.ack_time('now');
232 target.ack_staff(this.auth.user().id());
234 target.ischanged(true);
235 target.alert_type(source.alert_type());
236 target.temp(source.temp());
237 target.ack_time(source.ack_time());
238 target.note(source.note());
241 // clones everything but copy, create_time, and create_staff
242 // This is serving as a reference alert for the other matching alerts
243 cloneAlertForBatchProxy(source: IdlObject): IdlObject {
244 const target = this.idl.create('aca');
245 target.id( source.id() );
246 target.alert_type(source.alert_type());
247 target.temp(source.temp());
248 target.ack_time(source.ack_time());
249 target.ack_staff(source.ack_staff());
250 target.note(source.note());
256 const changedAlerts = [];
257 const changes = this.hasCopy()
260 ? this.alertsInCommon.filter(a => a.ischanged())
261 : this.copies[0].copy_alerts().filter(a => a.ischanged())
264 console.log('applyChanges, changes', changes);
266 changes.forEach(change => {
267 if (this.inBatch()) {
268 this.copies.forEach(copy => {
269 copy.copy_alerts().forEach(realAlert => {
270 // compare against the unchanged version of the reference alert
271 if (realAlert.id() !== change.id() && this.compositeMatch(realAlert, this.alertIdMap[ change.id() ])) {
272 this.setAlert(realAlert, change);
273 changedAlerts.push(realAlert);
277 // now change the original reference alert as well
278 this.setAlert(this.alertIdMap[ change.id() ], change);
279 changedAlerts.push( this.alertIdMap[ change.id() ] );
281 changedAlerts.push(change);
285 if (this.inPlaceCreateMode) {
286 this.close({ newAlerts: this.newAlerts, changedAlerts: changedAlerts });
289 console.log('changedAlerts.length, newAlerts.length', changedAlerts.length,this.newAlerts.length);
290 const pendingAlerts = changedAlerts;
292 this.newAlerts.forEach(alert => {
293 this.copies.forEach(c => {
294 const a = this.idl.clone(alert);
298 pendingAlerts.push(a);
302 this.pcrud.autoApply(pendingAlerts).toPromise().then(
304 this.successMsg.current().then(msg => this.toast.success(msg));
305 this.close({ newAlerts: this.newAlerts, changedAlerts: changedAlerts });
308 this.errorMsg.current().then(msg => this.toast.danger(msg));
309 console.error('pcrud error', err);