]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.service.ts
LP1888723 Remove ref. to nonexistent workstation setting
[working/Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / cat / volcopy / volcopy.service.ts
1 import {Injectable, EventEmitter} from '@angular/core';
2 import {Observable} from 'rxjs';
3 import {map, tap, mergeMap} from 'rxjs/operators';
4 import {IdlService, IdlObject} from '@eg/core/idl.service';
5 import {NetService} from '@eg/core/net.service';
6 import {OrgService} from '@eg/core/org.service';
7 import {PcrudService} from '@eg/core/pcrud.service';
8 import {EventService, EgEvent} from '@eg/core/event.service';
9 import {AuthService} from '@eg/core/auth.service';
10 import {VolCopyContext} from './volcopy';
11 import {HoldingsService, CallNumData} from '@eg/staff/share/holdings/holdings.service';
12 import {ServerStoreService} from '@eg/core/server-store.service';
13 import {StoreService} from '@eg/core/store.service';
14 import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component';
15
16 /* Managing volcopy data */
17
18 interface VolCopyDefaults {
19     // Default values per field.
20     values: {[field: string]: any};
21     // Most fields are visible by default.
22     hidden: {[field: string]: boolean};
23     // ... But some fields are hidden by default.
24     visible: {[field: string]: boolean};
25 }
26
27 @Injectable()
28 export class VolCopyService {
29
30     autoId = -1;
31
32     localOrgs: number[];
33     defaults: VolCopyDefaults = null;
34     copyStatuses: {[id: number]: IdlObject} = {};
35     bibParts: {[bibId: number]: IdlObject[]} = {};
36
37     // This will be all 'local' copy locations plus any remote
38     // locations that we are required to interact with.
39     copyLocationMap: {[id: number]: IdlObject} = {};
40
41     // Track this here so it can survive route changes.
42     currentContext: VolCopyContext;
43
44     statCatEntryMap: {[id: number]: IdlObject} = {}; // entry id => entry
45
46     templateNames: ComboboxEntry[] = [];
47     templates: any = {};
48
49     commonData: {[key: string]: IdlObject[]} = {};
50     magicCopyStats: number[] = [];
51
52     hideVolOrgs: number[] = [];
53
54     // Currently spans from volcopy.component to vol-edit.component.
55     genBarcodesRequested: EventEmitter<void> = new EventEmitter<void>();
56
57     constructor(
58         private evt: EventService,
59         private net: NetService,
60         private idl: IdlService,
61         private org: OrgService,
62         private auth: AuthService,
63         private pcrud: PcrudService,
64         private holdings: HoldingsService,
65         private store: StoreService,
66         private serverStore: ServerStoreService
67     ) {}
68
69
70     // Fetch the data that is always needed.
71     load(): Promise<any> {
72
73         if (this.commonData.acp_item_type_map) { return Promise.resolve(); }
74
75         this.localOrgs = this.org.fullPath(this.auth.user().ws_ou(), true);
76
77         this.hideVolOrgs = this.org.list()
78             .filter(o => !this.org.canHaveVolumes(o)).map(o => o.id());
79
80         return this.net.request(
81             'open-ils.cat', 'open-ils.cat.volcopy.data', this.auth.token()
82         ).pipe(tap(dataset => {
83             const key = Object.keys(dataset)[0];
84             this.commonData[key] = dataset[key];
85         })).toPromise()
86         .then(_ => this.ingestCommonData())
87
88         // These will come up later -- prefetch.
89         .then(_ => this.serverStore.getItemBatch([
90             'cat.copy.templates',
91             'eg.cat.volcopy.defaults',
92             'eg.cat.record.summary.collapse'
93         ]))
94
95         .then(_ => this.holdings.getMagicCopyStatuses())
96         .then(stats => this.magicCopyStats = stats)
97         .then(_ => this.fetchDefaults())
98         .then(_ => this.fetchTemplates());
99     }
100
101     ingestCommonData() {
102
103         this.commonData.acp_location.forEach(
104             loc => this.copyLocationMap[loc.id()] = loc);
105
106         // Remove the -1 prefix and suffix so they can be treated
107         // specially in the markup.
108         this.commonData.acn_prefix =
109             this.commonData.acn_prefix.filter(pfx => pfx.id() !== -1);
110
111         this.commonData.acn_suffix =
112             this.commonData.acn_suffix.filter(sfx => sfx.id() !== -1);
113
114         this.commonData.acp_status.forEach(
115             stat => this.copyStatuses[stat.id()] = stat);
116
117         this.commonData.acp_stat_cat.forEach(cat => {
118             cat.entries().forEach(
119                 entry => this.statCatEntryMap[entry.id()] = entry);
120         });
121     }
122
123     getLocation(id: number): Promise<IdlObject> {
124         if (this.copyLocationMap[id]) {
125             return Promise.resolve(this.copyLocationMap[id]);
126         }
127
128         return this.pcrud.retrieve('acpl', id)
129             .pipe(tap(loc => this.copyLocationMap[loc.id()] = loc))
130             .toPromise();
131     }
132
133     fetchTemplates(): Promise<any> {
134
135         // First check for local copy templates, since server-side
136         // templates are new w/ this code.  Move them to the server.
137         const tmpls = this.store.getLocalItem('cat.copy.templates');
138
139         const promise = tmpls ?
140             this.serverStore.setItem('cat.copy.templates', tmpls) :
141             Promise.resolve();
142
143         return promise
144         .then(_ => this.serverStore.getItem('cat.copy.templates'))
145         .then(templates => {
146
147             if (!templates) { return null; }
148
149             this.templates = templates;
150
151             this.templateNames = Object.keys(templates)
152             .sort((n1, n2) => n1 < n2 ? -1 : 1)
153             .map(name => ({id: name, label: name}));
154
155             this.store.removeLocalItem('cat.copy.templates');
156         });
157     }
158
159
160     saveTemplates(): Promise<any> {
161         this.store.setLocalItem('cat.copy.templates', this.templates);
162         // Re-sort, etc.
163         return this.fetchTemplates();
164     }
165
166     fetchDefaults(): Promise<any> {
167         if (this.defaults) { return Promise.resolve(); }
168
169         return this.serverStore.getItem('eg.cat.volcopy.defaults').then(
170             (defaults: VolCopyDefaults) => {
171                 this.defaults = defaults || {values: {}, hidden: {}, visible: {}};
172                 if (!this.defaults.values)  { this.defaults.values  = {}; }
173                 if (!this.defaults.hidden)  { this.defaults.hidden  = {}; }
174                 if (!this.defaults.visible) { this.defaults.visible = {}; }
175             }
176         );
177     }
178
179     // Fetch vol labels for a single record based on the defeault
180     // classification scheme
181     fetchRecordVolLabels(id: number): Promise<string[]> {
182         if (!id) { return Promise.resolve([]); }
183
184         // NOTE: see https://bugs.launchpad.net/evergreen/+bug/1874897
185         // for more on MARC call numbers and classification scheme.
186         // If there is no workstation-default value, pass null
187         // to use the org unit default.
188
189         return this.net.request(
190             'open-ils.cat',
191             'open-ils.cat.biblio.record.marc_cn.retrieve',
192             id, this.defaults.values.classification || null
193         ).toPromise().then(res => {
194             return Object.values(res)
195                 .map(blob => Object.values(blob)[0]).sort();
196         });
197     }
198
199     createStubVol(recordId: number, orgId: number, options?: any): IdlObject {
200         if (!options) { options = {}; }
201
202         const vol = this.idl.create('acn');
203         vol.id(this.autoId--);
204         vol.isnew(true);
205         vol.record(recordId);
206         vol.label(null);
207         vol.owning_lib(Number(orgId));
208         vol.prefix(this.defaults.values.prefix || -1);
209         vol.suffix(this.defaults.values.suffix || -1);
210
211         return vol;
212     }
213
214     createStubCopy(vol: IdlObject, options?: any): IdlObject {
215         if (!options) { options = {}; }
216
217         const copy = this.idl.create('acp');
218         copy.id(this.autoId--);
219         copy.isnew(true);
220         copy.call_number(vol); // fleshed
221         copy.price('0.00');
222         copy.deposit_amount('0.00');
223         copy.fine_level(2);     // Normal
224         copy.loan_duration(2);  // Normal
225         copy.location(this.commonData.acp_default_location); // fleshed
226         copy.circ_lib(Number(options.circLib || vol.owning_lib()));
227
228         copy.deposit('f');
229         copy.circulate('t');
230         copy.holdable('t');
231         copy.opac_visible('t');
232         copy.ref('f');
233         copy.mint_condition('t');
234
235         copy.parts([]);
236         copy.tags([]);
237         copy.notes([]);
238         copy.stat_cat_entries([]);
239
240         return copy;
241     }
242
243
244     // Applies label_class values to a batch of volumes, followed by
245     // applying labels to vols that need it.
246     setVolClassLabels(vols: IdlObject[]): Promise<any> {
247
248         return this.applyVolClasses(vols)
249         .then(_ => this.applyVolLabels(vols));
250     }
251
252     // Apply label_class values to any vols that need it based either on
253     // the workstation default value or the org setting for the
254     // owning lib library.
255     applyVolClasses(vols: IdlObject[]): Promise<any> {
256
257         vols = vols.filter(v => !v.label_class());
258
259         const orgIds: any = {};
260         vols.forEach(vol => orgIds[vol.owning_lib()] = true);
261
262         let promise = Promise.resolve(); // Serialization
263
264         if (this.defaults.values.classification) {
265             // Workstation default classification overrides the
266             // classification that might be used at the owning lib.
267
268             vols.forEach(vol =>
269                 vol.label_class(this.defaults.values.classification));
270
271             return promise;
272
273         } else {
274
275             // Get the label class default for each owning lib and
276             // apply to the volumes owned by that lib.
277
278             Object.keys(orgIds).map(orgId => Number(orgId))
279             .forEach(orgId => {
280                 promise = promise.then(_ => {
281
282                     return this.org.settings(
283                         'cat.default_classification_scheme', orgId)
284                     .then(sets => {
285
286                         const orgVols = vols.filter(v => v.owning_lib() === orgId);
287                         orgVols.forEach(vol => {
288                             vol.label_class(
289                                 sets['cat.default_classification_scheme'] || 1
290                             );
291                         });
292                     });
293                 });
294             });
295         }
296
297         return promise;
298     }
299
300     // Apply labels to volumes based on the appropriate MARC call number.
301     applyVolLabels(vols: IdlObject[]): Promise<any> {
302
303         vols = vols.filter(v => !v.label());
304
305         // Serialize
306         let promise = Promise.resolve();
307
308         vols.forEach(vol => {
309
310             // Avoid unnecessary lookups.
311             // Note the label may have been applied to this volume
312             // in a previous iteration of this loop.
313             if (vol.label()) { return; }
314
315             // Avoid applying call number labels to existing call numbers
316             // that don't already have a label.  This allows the user to
317             // see that an action needs to be taken on the volume.
318             if (!vol.isnew()) { return; }
319
320             promise = promise.then(_ => {
321                 return this.net.request(
322                     'open-ils.cat',
323                     'open-ils.cat.biblio.record.marc_cn.retrieve',
324                     vol.record(), vol.label_class()).toPromise()
325
326                 .then(cnList => {
327                     // Use '_' as a placeholder to indicate when a
328                     // vol has already been addressed.
329                     let label = '_';
330
331                     if (cnList.length > 0) {
332                         const field = Object.keys(cnList[0])[0];
333                         label = cnList[0][field];
334                     }
335
336                     // Avoid making duplicate marc_cn calls by applying
337                     // the label to all vols that apply.
338                     vols.forEach(vol2 => {
339                         if (vol2.record() === vol.record() &&
340                             vol2.label_class() === vol.label_class()) {
341                             vol.label(label);
342                         }
343                     });
344                 });
345             });
346         });
347
348         return promise.then(_ => {
349             // Remove the placeholder label
350             vols.forEach(vol => {
351                 if (vol.label() === '_') { vol.label(''); }
352             });
353         });
354     }
355
356     // Sets the default copy status for a batch of copies.
357     setCopyStatus(copies: IdlObject[]): Promise<any> {
358
359         const fastAdd = this.currentContext.fastAdd;
360
361         const setting = fastAdd ?
362             'cat.default_copy_status_fast' :
363             'cat.default_copy_status_normal';
364
365         const orgs: any = {};
366         copies.forEach(copy => orgs[copy.circ_lib()] = 1);
367
368         let promise = Promise.resolve(); // Seralize
369
370         // Pre-fetch needed org settings
371         Object.keys(orgs).forEach(org => {
372             promise = promise.then(_ => {
373                 return this.org.settings(setting, +org)
374                 .then(sets => {
375                     orgs[org] = sets[setting] || (fastAdd ? 0 : 5);
376                 });
377             });
378         });
379
380         promise.then(_ => {
381             Object.keys(orgs).forEach(org => {
382                 copies.filter(copy => copy.circ_lib() === +org)
383                 .forEach(copy => copy.status(orgs[org]));
384             });
385         });
386
387         return promise;
388     }
389
390     saveDefaults(): Promise<any> {
391
392         // Scrub unnecessary content before storing.
393
394         Object.keys(this.defaults.values).forEach(field => {
395             if (this.defaults.values[field] === null) {
396                 delete this.defaults.values[field];
397             }
398         });
399
400         Object.keys(this.defaults.hidden).forEach(field => {
401             if (this.defaults.hidden[field] !== true) {
402                 delete this.defaults.hidden[field];
403             }
404         });
405
406         return this.serverStore.setItem(
407             'eg.cat.volcopy.defaults', this.defaults);
408     }
409
410     fetchBibParts(recordIds: number[]) {
411
412         if (recordIds.length === 0) { return; }
413
414         // All calls fetch updated data since we may be creating
415         // new mono parts during editing.
416
417         this.pcrud.search('bmp',
418             {record: recordIds, deleted: 'f'})
419         .subscribe(
420             part => {
421                 if (!this.bibParts[part.record()]) {
422                     this.bibParts[part.record()] = [];
423                 }
424                 this.bibParts[part.record()].push(part);
425             },
426             err => {},
427             () => {
428                 recordIds.forEach(bibId => {
429                     if (this.bibParts[bibId]) {
430                         this.bibParts[bibId] = this.bibParts[bibId]
431                         .sort((p1, p2) =>
432                             p1.label_sortkey() < p2.label_sortkey() ? -1 : 1);
433                     }
434                 });
435             }
436         );
437     }
438
439
440     copyStatIsMagic(statId: number): boolean {
441         return this.magicCopyStats.includes(statId);
442     }
443
444     restrictCopyDelete(statId: number): boolean {
445         return this.copyStatuses[statId] &&
446                this.copyStatuses[statId].restrict_copy_delete() === 't';
447     }
448 }
449