]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/share/holdings/copy-alerts-dialog.component.ts
LP2061136 - Stamping 1405 DB upgrade script
[Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / share / holdings / copy-alerts-dialog.component.ts
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';
14
15 /**
16  * Dialog for managing copy alerts.
17  */
18
19 export interface CopyAlertsChanges {
20     newAlerts: IdlObject[];
21     changedAlerts: IdlObject[];
22 }
23
24 @Component({
25     selector: 'eg-copy-alerts-dialog',
26     templateUrl: 'copy-alerts-dialog.component.html'
27 })
28
29 export class CopyAlertsDialogComponent
30     extends DialogComponent {
31
32     @Input() copyIds: number[] = [];
33     copies: IdlObject[];
34     alertIdMap: { [key: number]: any };
35
36     mode: string; // create | manage
37
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;
41
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[] = [];
46
47     alertTypes: ComboboxEntry[];
48     newAlert: IdlObject;
49     newAlerts: IdlObject[];
50     autoId = -1;
51     changesMade: boolean;
52     defaultAlertType = 1; // default default :-)
53
54     @ViewChild('successMsg', { static: true }) private successMsg: StringComponent;
55     @ViewChild('errorMsg', { static: true }) private errorMsg: StringComponent;
56
57     constructor(
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
66         this.copyIds = [];
67         this.copies = [];
68         this.alertIdMap = {};
69     }
70
71     prepNewAlert(): IdlObject {
72         const newAlert = this.idl.create('aca');
73         newAlert.alert_type(this.defaultAlertType);
74         newAlert.create_staff(this.auth.user().id());
75         return newAlert;
76     }
77
78     hasCopy(): Boolean {
79         return this.copies.length > 0;
80     }
81
82     inBatch(): Boolean {
83         return this.copies.length > 1;
84     }
85
86     /**
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.
90      */
91     open(args: NgbModalOptions): Observable<CopyAlertsChanges> {
92         this.copies = [];
93         this.newAlerts = [];
94         this.newAlert = this.prepNewAlert();
95
96         if (this.copyIds.length === 0 && !this.inPlaceCreateMode) {
97             return throwError('copy ID required');
98         }
99
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';
105
106         // Observerify data loading
107         const obs = from(
108             this.getAlertTypes()
109                 .then(_ => this.getCopies())
110                 .then(_ => this.getCopyAlerts())
111                 .then(_ => this.getDefaultAlertType())
112                 .then(_ => { if (this.defaultAlertType) { this.newAlert.alert_type(this.defaultAlertType); } })
113         );
114
115         // Return open() observable to caller
116         return obs.pipe(switchMap(_ => super.open(args)));
117     }
118
119     getAlertTypes(): Promise<any> {
120         if (this.alertTypes) { return Promise.resolve(); }
121
122         return this.pcrud.retrieveAll('ccat',
123             {   active: true,
124                 scope_org: this.org.ancestors(this.auth.user().ws_ou(), true)
125             }, {atomic: true}
126         ).toPromise().then(alerts => {
127             this.alertTypes = alerts.map(a => ({id: a.id(), label: a.name()}));
128         });
129     }
130
131     getCopies(): Promise<any> {
132
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(); }
136
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([]));
141             });
142     }
143
144     // Copy alerts for the selected copies which have not been
145     // acknowledged by staff and are within org unit range of
146     // the alert type.
147     getCopyAlerts(): Promise<any> {
148         const typeIds = this.alertTypes.map(a => a.id);
149
150         return this.pcrud.search('aca',
151             {copy: this.copyIds, ack_time: null, alert_type: typeIds},
152             {}, {atomic: true})
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);
158                 });
159                 if (this.inBatch()) {
160                     let potentialMatches = this.copies[0].copy_alerts();
161
162                     this.copies.slice(1).forEach(copy => {
163                         potentialMatches = potentialMatches.filter(alertFromFirstCopy =>
164                             copy.copy_alerts().some(alertFromCurrentCopy =>
165                                 this.compositeMatch(alertFromFirstCopy, alertFromCurrentCopy)
166                             )
167                         );
168                     });
169
170                     // potentialMatches now contains alerts that have a "match" in every copy
171                     this.alertsInCommon = potentialMatches.map( match => this.cloneAlertForBatchProxy(match) );
172                 }
173             });
174     }
175
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
183         //      service.
184         return this.serverStore.getItem('eg.cat.volcopy.defaults').then(
185             (defaults) => {
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;
190                 }
191             }
192         );
193     }
194
195     getAlertTypeLabel(alert: IdlObject): string {
196         const alertType = this.alertTypes.filter(t => t.id === alert.alert_type());
197         return alertType[0].label;
198     }
199
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());
205     }
206
207     // Add the in-progress new note to all copies.
208     addNew() {
209
210         this.newAlert.id(this.autoId--);
211         this.newAlert.isnew(true);
212         this.newAlerts.push(this.newAlert);
213
214         this.newAlert = this.prepNewAlert();
215
216     }
217
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()
222             && (
223                 (a.ack_time() === null && b.ack_time() === null)
224                     || (a.ack_time() !== null && b.ack_time() !== null)
225             );
226     }
227
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());
233         }
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());
239     }
240
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());
251         return target;
252     }
253
254     applyChanges() {
255
256         const changedAlerts = [];
257         const changes = this.hasCopy()
258             ? (
259                 this.inBatch()
260                     ? this.alertsInCommon.filter(a => a.ischanged())
261                     : this.copies[0].copy_alerts().filter(a => a.ischanged())
262             )
263             : [];
264         console.log('applyChanges, changes', changes);
265
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);
274                         }
275                     });
276                 });
277                 // now change the original reference alert as well
278                 this.setAlert(this.alertIdMap[ change.id() ], change);
279                 changedAlerts.push( this.alertIdMap[ change.id() ] );
280             } else {
281                 changedAlerts.push(change);
282             }
283         });
284
285         if (this.inPlaceCreateMode) {
286             this.close({ newAlerts: this.newAlerts, changedAlerts: changedAlerts });
287             return;
288         }
289         console.log('changedAlerts.length, newAlerts.length', changedAlerts.length,this.newAlerts.length);
290         const pendingAlerts = changedAlerts;
291
292         this.newAlerts.forEach(alert => {
293             this.copies.forEach(c => {
294                 const a = this.idl.clone(alert);
295                 a.isnew(true);
296                 a.id(null);
297                 a.copy(c.id());
298                 pendingAlerts.push(a);
299             });
300         });
301
302         this.pcrud.autoApply(pendingAlerts).toPromise().then(
303             ok => {
304                 this.successMsg.current().then(msg => this.toast.success(msg));
305                 this.close({ newAlerts: this.newAlerts, changedAlerts: changedAlerts });
306             },
307             err => {
308                 this.errorMsg.current().then(msg => this.toast.danger(msg));
309                 console.error('pcrud error', err);
310             }
311         );
312     }
313 }