]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/share/item-location-select/item-location-select.component.ts
LP1929741 ACQ Selection List & PO Angluar Port
[Evergreen.git] / Open-ILS / src / eg2 / src / app / share / item-location-select / item-location-select.component.ts
1 import {Component, OnInit, AfterViewInit, Input, Output, ViewChild,
2     EventEmitter, forwardRef} from '@angular/core';
3 import {ControlValueAccessor, FormGroup, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';
4 import {Observable, from, of} from 'rxjs';
5 import {tap, map, switchMap} from 'rxjs/operators';
6 import {IdlObject} from '@eg/core/idl.service';
7 import {OrgService} from '@eg/core/org.service';
8 import {AuthService} from '@eg/core/auth.service';
9 import {PermService} from '@eg/core/perm.service';
10 import {PcrudService} from '@eg/core/pcrud.service';
11 import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component';
12 import {StringComponent} from '@eg/share/string/string.component';
13 import {ItemLocationService} from './item-location-select.service';
14
15 /**
16  * Item (Copy) Location Selector.
17  *
18  * <eg-item-location-select [(ngModel)]="myAcplId"
19     [contextOrgId]="anOrgId" permFilter="ADMIN_STUFF">
20  * </eg-item-location-select>
21  */
22
23 @Component({
24   selector: 'eg-item-location-select',
25   templateUrl: './item-location-select.component.html',
26   providers: [{
27       provide: NG_VALUE_ACCESSOR,
28       useExisting: forwardRef(() => ItemLocationSelectComponent),
29       multi: true
30   }]
31 })
32 export class ItemLocationSelectComponent
33     implements OnInit, AfterViewInit, ControlValueAccessor {
34     static domIdAuto = 0;
35
36     // Limit copy locations to those owned at or above org units where
37     // the user has work permissions for the provided permission code.
38     @Input() permFilter: string;
39
40     // Limit copy locations to those owned at or above this org unit.
41     private _contextOrgId: number;
42     @Input() set contextOrgId(value: number) {
43         this._contextOrgId = value;
44         this.ngOnInit();
45     }
46
47     get contextOrgId(): number {
48         return this._contextOrgId;
49     }
50
51     // Load locations for multiple context org units.
52     private _contextOrgIds = [];
53     @Input() set contextOrgIds(value: number[]) {
54         this._contextOrgIds = value;
55     }
56
57     get contextOrgIds(): number[] {
58         return this._contextOrgIds;
59     }
60
61     @Input() orgUnitLabelField = 'shortname';
62
63     // Emits an acpl object or null on combobox value change
64     @Output() valueChange: EventEmitter<IdlObject>;
65
66     @Input() required: boolean;
67
68     @Input() domId = 'eg-item-location-select-' +
69         ItemLocationSelectComponent.domIdAuto++;
70
71     // If false, selector will be click-able
72     @Input() loadAsync = true;
73
74     @Input() disabled = false;
75
76     // Display the selected value as text instead of within
77     // the typeahead
78     @Input() readOnly = false;
79
80     @ViewChild('comboBox', {static: false}) comboBox: ComboboxComponent;
81     @ViewChild('unsetString', {static: false}) unsetString: StringComponent;
82
83     @Input() startId: number = null;
84     filterOrgs: number[] = [];
85     filterOrgsApplied = false;
86
87     initDone = false; // true after first data load
88     propagateChange = (id: number) => {};
89     propagateTouch = () => {};
90
91     getLocationsAsyncHandler = term => this.getLocationsAsync(term);
92
93     constructor(
94         private org: OrgService,
95         private auth: AuthService,
96         private perm: PermService,
97         private pcrud: PcrudService,
98         private loc: ItemLocationService
99     ) {
100         this.valueChange = new EventEmitter<IdlObject>();
101     }
102
103     ngOnInit() {
104         if (this.loadAsync) {
105             this.initDone = true;
106         } else {
107             this.setFilterOrgs()
108             .then(_ => this.getLocations())
109             .then(_ => this.initDone = true);
110         }
111     }
112
113     ngAfterViewInit() {
114
115         // Format the display of locations to include the org unit
116         this.comboBox.formatDisplayString = (result: ComboboxEntry) => {
117             let display = result.label || result.id;
118             display = (display + '').trim();
119             if (result.userdata) {
120                 display += ' (' +
121                     this.orgName(result.userdata.owning_lib()) + ')';
122             }
123             return display;
124         };
125     }
126
127     getLocations(): Promise<any> {
128
129         if (this.filterOrgs.length === 0) {
130             this.comboBox.entries = [];
131             return Promise.resolve();
132         }
133
134         const search: any = {deleted: 'f'};
135
136         if (this.startId) {
137             // Guarantee we have the load-time copy location, which
138             // may not be included in the org-scoped set of locations
139             // we fetch by default.
140             search['-or'] = [
141                 {id: this.startId},
142                 {owning_lib: this.filterOrgs}
143             ];
144         } else {
145             search.owning_lib = this.filterOrgs;
146         }
147
148         const entries: ComboboxEntry[] = [];
149
150         if (!this.required) {
151             entries.push({id: null, label: this.unsetString.text});
152         }
153
154         return this.pcrud.search('acpl', search, {order_by: {acpl: 'name'}}
155         ).pipe(map(loc => {
156             this.loc.locationCache[loc.id()] = loc;
157             entries.push({id: loc.id(), label: loc.name(), userdata: loc});
158         })).toPromise().then(_ => {
159             this.comboBox.entries = entries;
160         });
161     }
162
163     getLocationsAsync(term: string): Observable<ComboboxEntry> {
164
165         let obs = of();
166         if (!this.filterOrgsApplied) {
167             // Apply filter orgs the first time they are needed.
168             obs = from(this.setFilterOrgs());
169         }
170
171         return obs.pipe(switchMap(_ => this.getLocationsAsync2(term)));
172     }
173
174     getLocationsAsync2(term: string): Observable<ComboboxEntry> {
175
176         if (this.filterOrgs.length === 0) {
177             return of();
178         }
179
180         const search: any = {
181             deleted: 'f',
182             name: {'ilike': `%${term}%`}
183         };
184
185         if (this.startId) {
186             // Guarantee we have the load-time copy location, which
187             // may not be included in the org-scoped set of locations
188             // we fetch by default.
189             search['-or'] = [
190                 {id: this.startId},
191                 {owning_lib: this.filterOrgs}
192             ];
193         } else {
194             search.owning_lib = this.filterOrgs;
195         }
196
197         return new Observable<ComboboxEntry>(observer => {
198             if (!this.required) {
199                 observer.next({id: null, label: this.unsetString.text});
200             }
201
202             this.pcrud.search('acpl', search, {order_by: {acpl: 'name'}}
203             ).subscribe(
204                 loc => {
205                     this.loc.locationCache[loc.id()] = loc;
206                     observer.next({id: loc.id(), label: loc.name(), userdata: loc});
207                 },
208                 err => {},
209                 () => observer.complete()
210             );
211         });
212     }
213
214
215     registerOnChange(fn) {
216         this.propagateChange = fn;
217     }
218
219     registerOnTouched(fn) {
220         this.propagateTouch = fn;
221     }
222
223     cboxChanged(entry: ComboboxEntry) {
224         const id = entry ? entry.id : null;
225         this.propagateChange(id);
226         this.valueChange.emit(id ? this.loc.locationCache[id] : null);
227     }
228
229     writeValue(id: number) {
230         if (this.initDone) {
231             this.getOneLocation(id).then(_ => this.comboBox.selectedId = id);
232         } else {
233             this.startId = id;
234         }
235     }
236
237     getOneLocation(id: number) {
238         if (!id) { return Promise.resolve(); }
239
240         const promise = this.loc.locationCache[id] ?
241             Promise.resolve(this.loc.locationCache[id]) :
242             this.pcrud.retrieve('acpl', id).toPromise();
243
244         return promise.then(loc => {
245
246             this.loc.locationCache[loc.id()] = loc;
247             const entry: ComboboxEntry = {
248                 id: loc.id(), label: loc.name(), userdata: loc};
249
250             if (this.comboBox.entries) {
251                 this.comboBox.entries.push(entry);
252             } else {
253                 this.comboBox.entries = [entry];
254             }
255         });
256     }
257
258     setFilterOrgs(): Promise<number[]> {
259         let contextOrgIds: number[] = [];
260
261         if (this.contextOrgIds.length) {
262             contextOrgIds = this.contextOrgIds;
263         } else {
264             contextOrgIds = [this.contextOrgId || this.auth.user().ws_ou()];
265         }
266
267         let orgIds = [];
268         contextOrgIds.forEach(id => orgIds = orgIds.concat(this.org.ancestors(id, true)));
269
270         this.filterOrgsApplied = true;
271
272         if (!this.permFilter) {
273             return Promise.resolve(this.filterOrgs = [...new Set(orgIds)]);
274         }
275
276         const orgsFromCache = this.loc.filterOrgsCache[this.permFilter];
277         if (orgsFromCache) {
278             return Promise.resolve(this.filterOrgs = orgsFromCache);
279         }
280
281         return this.perm.hasWorkPermAt([this.permFilter], true)
282         .then(values => {
283             // Include ancestors of perm-approved org units (shared item locations)
284
285             const permOrgIds = values[this.permFilter];
286             let trimmedOrgIds = [];
287             permOrgIds.forEach(orgId => {
288                 if (orgIds.includes(orgId)) {
289                     trimmedOrgIds = trimmedOrgIds.concat(this.org.ancestors(orgId, true));
290                 }
291             });
292
293             this.filterOrgs = [...new Set(trimmedOrgIds)];
294             this.loc.filterOrgsCache[this.permFilter] = this.filterOrgs;
295
296             return this.filterOrgs;
297         });
298     }
299
300     orgName(orgId: number): string {
301         return this.org.get(orgId)[this.orgUnitLabelField]();
302     }
303 }
304
305
306