]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/share/holdings/copy-alerts-dialog.component.ts
LP#1956986: fix and refactor item alert handing in Angular editor
[Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / share / holdings / copy-alerts-dialog.component.ts
1 import {Component, OnInit, Input, ViewChild} from '@angular/core';
2 import {Observable, throwError, from} from 'rxjs';
3 import {switchMap} from 'rxjs/operators';
4 import {NetService} from '@eg/core/net.service';
5 import {IdlService, IdlObject} from '@eg/core/idl.service';
6 import {EventService} from '@eg/core/event.service';
7 import {ToastService} from '@eg/share/toast/toast.service';
8 import {AuthService} from '@eg/core/auth.service';
9 import {PcrudService} from '@eg/core/pcrud.service';
10 import {OrgService} from '@eg/core/org.service';
11 import {StringComponent} from '@eg/share/string/string.component';
12 import {DialogComponent} from '@eg/share/dialog/dialog.component';
13 import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
14 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
15
16 /**
17  * Dialog for managing copy alerts.
18  */
19
20 export interface CopyAlertsChanges {
21     newAlerts: IdlObject[];
22     changedAlerts: IdlObject[];
23 }
24
25 @Component({
26   selector: 'eg-copy-alerts-dialog',
27   templateUrl: 'copy-alerts-dialog.component.html'
28 })
29
30 export class CopyAlertsDialogComponent
31     extends DialogComponent implements OnInit {
32
33     // If there are multiple copyIds, only new alerts may be applied.
34     // If there is only one copyId, then alerts may be applied or removed.
35     @Input() copyIds: number[] = [];
36
37     mode: string; // create | manage
38
39     // If true, no attempt is made to save new alerts to the
40     // database.  It's assumed this takes place in the calling code.
41     @Input() inPlaceCreateMode = false;
42
43     // In 'create' mode, we may be adding notes to multiple copies.
44     copies: IdlObject[];
45     // In 'manage' mode we only handle a single copy.
46     copy: IdlObject;
47
48     alertTypes: ComboboxEntry[];
49     newAlert: IdlObject;
50     newAlerts: IdlObject[];
51     autoId = -1;
52     changesMade: boolean;
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 net: NetService,
61         private idl: IdlService,
62         private pcrud: PcrudService,
63         private org: OrgService,
64         private auth: AuthService) {
65         super(modal); // required for subclassing
66         this.copyIds = [];
67         this.copies = [];
68     }
69
70     ngOnInit() {}
71
72     /**
73      * Fetch the item/record, then open the dialog.
74      * Dialog promise resolves with true/false indicating whether
75      * the mark-damanged action occured or was dismissed.
76      */
77     open(args: NgbModalOptions): Observable<CopyAlertsChanges> {
78         this.copy = null;
79         this.copies = [];
80         this.newAlert = this.idl.create('aca');
81         this.newAlerts = [];
82         this.newAlert.create_staff(this.auth.user().id());
83
84         if (this.copyIds.length === 0 && !this.inPlaceCreateMode) {
85             return throwError('copy ID required');
86         }
87
88         // In manage mode, we can only manage a single copy.
89         // But in create mode, we can add alerts to multiple copies.
90         // We can only manage copies that already exist in the database.
91         if (this.copyIds.length === 1 && this.copyIds[0] > 0) {
92             this.mode = 'manage';
93         } else {
94             this.mode = 'create';
95         }
96
97         // Observerify data loading
98         const obs = from(
99             this.getAlertTypes()
100             .then(_ => this.getCopies())
101             .then(_ => this.mode === 'manage' ? this.getCopyAlerts() : null)
102         );
103
104         // Return open() observable to caller
105         return obs.pipe(switchMap(_ => super.open(args)));
106     }
107
108     getAlertTypes(): Promise<any> {
109         if (this.alertTypes) { return Promise.resolve(); }
110
111         return this.pcrud.retrieveAll('ccat',
112         {   active: true,
113             scope_org: this.org.ancestors(this.auth.user().ws_ou(), true)
114         }, {atomic: true}
115         ).toPromise().then(alerts => {
116             this.alertTypes = alerts.map(a => ({id: a.id(), label: a.name()}));
117         });
118     }
119
120     getCopies(): Promise<any> {
121
122         // Avoid fetch if we're only adding notes to isnew copies.
123         const ids = this.copyIds.filter(id => id > 0);
124         if (ids.length === 0) { return Promise.resolve(); }
125
126         return this.pcrud.search('acp', {id: this.copyIds}, {}, {atomic: true})
127         .toPromise().then(copies => {
128             this.copies = copies;
129             copies.forEach(c => c.copy_alerts([]));
130             if (this.mode === 'manage') {
131                 this.copy = copies[0];
132             }
133         });
134     }
135
136     // Copy alerts for the selected copies which have not been
137     // acknowledged by staff and are within org unit range of
138     // the alert type.
139     getCopyAlerts(): Promise<any> {
140         const typeIds = this.alertTypes.map(a => a.id);
141
142         return this.pcrud.search('aca',
143             {copy: this.copyIds, ack_time: null, alert_type: typeIds},
144             {}, {atomic: true})
145         .toPromise().then(alerts => {
146             alerts.forEach(a => {
147                 const copy = this.copies.filter(c => c.id() === a.copy())[0];
148                 copy.copy_alerts().push(a);
149             });
150         });
151     }
152
153     getAlertTypeLabel(alert: IdlObject): string {
154         const alertType = this.alertTypes.filter(t => t.id === alert.alert_type());
155         return alertType[0].label;
156     }
157
158     removeAlert(alert: IdlObject) {
159         // the only type of alerts we can remove are pending ones that
160         // we have created during the lifetime of this modal; alerts
161         // that already exist can only be cleared
162         this.newAlerts = this.newAlerts.filter(t => t.id() !== alert.id());
163     }
164
165     // Add the in-progress new note to all copies.
166     addNew() {
167         if (!this.newAlert.alert_type()) { return; }
168
169         this.newAlert.id(this.autoId--);
170         this.newAlert.isnew(true);
171         this.newAlerts.push(this.newAlert);
172
173         this.newAlert = this.idl.create('aca');
174
175     }
176
177     applyChanges() {
178
179         const changedAlerts = this.copy ?
180             this.copy.copy_alerts().filter(a => a.ischanged()) :
181             [];
182         if (this.inPlaceCreateMode) {
183             this.close({ newAlerts: this.newAlerts, changedAlerts: changedAlerts });
184             return;
185         }
186
187         const alerts = [];
188         this.newAlerts.forEach(alert => {
189             this.copies.forEach(c => {
190                 const a = this.idl.clone(alert);
191                 a.isnew(true);
192                 a.id(null);
193                 a.copy(c.id());
194                 alerts.push(a);
195             });
196         });
197         if (this.mode === 'manage') {
198             changedAlerts.forEach(alert => {
199                 alerts.push(alert);
200             });
201         }
202         this.pcrud.autoApply(alerts).toPromise().then(
203             ok => {
204                 this.successMsg.current().then(msg => this.toast.success(msg));
205                 this.close({ newAlerts: this.newAlerts, changedAlerts: changedAlerts });
206             },
207             err => this.errorMsg.current().then(msg => this.toast.danger(msg))
208         );
209     }
210 }