]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.service.ts
LP1951162 Migrate copy templates 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         return this.serverStore.getItem('cat.copy.templates')
136         .then(templates => {
137
138             if (!templates) { return null; }
139
140             this.templates = templates;
141
142             this.templateNames = Object.keys(templates)
143             .sort((n1, n2) => n1 < n2 ? -1 : 1)
144             .map(name => ({id: name, label: name}));
145
146             this.store.removeLocalItem('cat.copy.templates');
147         });
148     }
149
150
151     saveTemplates(): Promise<any> {
152         this.store.setLocalItem('cat.copy.templates', this.templates);
153         // Re-sort, etc.
154         return this.fetchTemplates();
155     }
156
157     fetchDefaults(): Promise<any> {
158         if (this.defaults) { return Promise.resolve(); }
159
160         return this.serverStore.getItem('eg.cat.volcopy.defaults').then(
161             (defaults: VolCopyDefaults) => {
162                 this.defaults = defaults || {values: {}, hidden: {}, visible: {}};
163                 if (!this.defaults.values)  { this.defaults.values  = {}; }
164                 if (!this.defaults.hidden)  { this.defaults.hidden  = {}; }
165                 if (!this.defaults.visible) { this.defaults.visible = {}; }
166             }
167         );
168     }
169
170     // Fetch vol labels for a single record based on the defeault
171     // classification scheme
172     fetchRecordVolLabels(id: number): Promise<string[]> {
173         if (!id) { return Promise.resolve([]); }
174
175         // NOTE: see https://bugs.launchpad.net/evergreen/+bug/1874897
176         // for more on MARC call numbers and classification scheme.
177         // If there is no workstation-default value, pass null
178         // to use the org unit default.
179
180         return this.net.request(
181             'open-ils.cat',
182             'open-ils.cat.biblio.record.marc_cn.retrieve',
183             id, this.defaults.values.classification || null
184         ).toPromise().then(res => {
185             return Object.values(res)
186                 .map(blob => Object.values(blob)[0]).sort();
187         });
188     }
189
190     createStubVol(recordId: number, orgId: number, options?: any): IdlObject {
191         if (!options) { options = {}; }
192
193         const vol = this.idl.create('acn');
194         vol.id(this.autoId--);
195         vol.isnew(true);
196         vol.record(recordId);
197         vol.label(null);
198         vol.owning_lib(Number(orgId));
199         vol.prefix(this.defaults.values.prefix || -1);
200         vol.suffix(this.defaults.values.suffix || -1);
201
202         return vol;
203     }
204
205     createStubCopy(vol: IdlObject, options?: any): IdlObject {
206         if (!options) { options = {}; }
207
208         const copy = this.idl.create('acp');
209         copy.id(this.autoId--);
210         copy.isnew(true);
211         copy.call_number(vol); // fleshed
212         copy.price('0.00');
213         copy.deposit_amount('0.00');
214         copy.fine_level(2);     // Normal
215         copy.loan_duration(2);  // Normal
216         copy.location(this.commonData.acp_default_location); // fleshed
217         copy.circ_lib(Number(options.circLib || vol.owning_lib()));
218
219         copy.deposit('f');
220         copy.circulate('t');
221         copy.holdable('t');
222         copy.opac_visible('t');
223         copy.ref('f');
224         copy.mint_condition('t');
225
226         copy.parts([]);
227         copy.tags([]);
228         copy.notes([]);
229         copy.stat_cat_entries([]);
230
231         return copy;
232     }
233
234
235     // Applies label_class values to a batch of volumes, followed by
236     // applying labels to vols that need it.
237     setVolClassLabels(vols: IdlObject[]): Promise<any> {
238
239         return this.applyVolClasses(vols)
240         .then(_ => this.applyVolLabels(vols));
241     }
242
243     // Apply label_class values to any vols that need it based either on
244     // the workstation default value or the org setting for the
245     // owning lib library.
246     applyVolClasses(vols: IdlObject[]): Promise<any> {
247
248         vols = vols.filter(v => !v.label_class());
249
250         const orgIds: any = {};
251         vols.forEach(vol => orgIds[vol.owning_lib()] = true);
252
253         let promise = Promise.resolve(); // Serialization
254
255         if (this.defaults.values.classification) {
256             // Workstation default classification overrides the
257             // classification that might be used at the owning lib.
258
259             vols.forEach(vol =>
260                 vol.label_class(this.defaults.values.classification));
261
262             return promise;
263
264         } else {
265
266             // Get the label class default for each owning lib and
267             // apply to the volumes owned by that lib.
268
269             Object.keys(orgIds).map(orgId => Number(orgId))
270             .forEach(orgId => {
271                 promise = promise.then(_ => {
272
273                     return this.org.settings(
274                         'cat.default_classification_scheme', orgId)
275                     .then(sets => {
276
277                         const orgVols = vols.filter(v => v.owning_lib() === orgId);
278                         orgVols.forEach(vol => {
279                             vol.label_class(
280                                 sets['cat.default_classification_scheme'] || 1
281                             );
282                         });
283                     });
284                 });
285             });
286         }
287
288         return promise;
289     }
290
291     // Apply labels to volumes based on the appropriate MARC call number.
292     applyVolLabels(vols: IdlObject[]): Promise<any> {
293
294         vols = vols.filter(v => !v.label());
295
296         // Serialize
297         let promise = Promise.resolve();
298
299         vols.forEach(vol => {
300
301             // Avoid unnecessary lookups.
302             // Note the label may have been applied to this volume
303             // in a previous iteration of this loop.
304             if (vol.label()) { return; }
305
306             // Avoid applying call number labels to existing call numbers
307             // that don't already have a label.  This allows the user to
308             // see that an action needs to be taken on the volume.
309             if (!vol.isnew()) { return; }
310
311             promise = promise.then(_ => {
312                 return this.net.request(
313                     'open-ils.cat',
314                     'open-ils.cat.biblio.record.marc_cn.retrieve',
315                     vol.record(), vol.label_class()).toPromise()
316
317                 .then(cnList => {
318                     // Use '_' as a placeholder to indicate when a
319                     // vol has already been addressed.
320                     let label = '_';
321
322                     if (cnList.length > 0) {
323                         const field = Object.keys(cnList[0])[0];
324                         label = cnList[0][field];
325                     }
326
327                     // Avoid making duplicate marc_cn calls by applying
328                     // the label to all vols that apply.
329                     vols.forEach(vol2 => {
330                         if (vol2.record() === vol.record() &&
331                             vol2.label_class() === vol.label_class()) {
332                             vol.label(label);
333                         }
334                     });
335                 });
336             });
337         });
338
339         return promise.then(_ => {
340             // Remove the placeholder label
341             vols.forEach(vol => {
342                 if (vol.label() === '_') { vol.label(''); }
343             });
344         });
345     }
346
347     // Sets the default copy status for a batch of copies.
348     setCopyStatus(copies: IdlObject[]): Promise<any> {
349
350         const fastAdd = this.currentContext.fastAdd;
351
352         const setting = fastAdd ?
353             'cat.default_copy_status_fast' :
354             'cat.default_copy_status_normal';
355
356         const orgs: any = {};
357         copies.forEach(copy => orgs[copy.circ_lib()] = 1);
358
359         let promise = Promise.resolve(); // Seralize
360
361         // Pre-fetch needed org settings
362         Object.keys(orgs).forEach(org => {
363             promise = promise.then(_ => {
364                 return this.org.settings(setting, +org)
365                 .then(sets => {
366                     orgs[org] = sets[setting] || (fastAdd ? 0 : 5);
367                 });
368             });
369         });
370
371         promise.then(_ => {
372             Object.keys(orgs).forEach(org => {
373                 copies.filter(copy => copy.circ_lib() === +org)
374                 .forEach(copy => copy.status(orgs[org]));
375             });
376         });
377
378         return promise;
379     }
380
381     saveDefaults(): Promise<any> {
382
383         // Scrub unnecessary content before storing.
384
385         Object.keys(this.defaults.values).forEach(field => {
386             if (this.defaults.values[field] === null) {
387                 delete this.defaults.values[field];
388             }
389         });
390
391         Object.keys(this.defaults.hidden).forEach(field => {
392             if (this.defaults.hidden[field] !== true) {
393                 delete this.defaults.hidden[field];
394             }
395         });
396
397         return this.serverStore.setItem(
398             'eg.cat.volcopy.defaults', this.defaults);
399     }
400
401     fetchBibParts(recordIds: number[]) {
402
403         if (recordIds.length === 0) { return; }
404
405         // All calls fetch updated data since we may be creating
406         // new mono parts during editing.
407
408         this.pcrud.search('bmp',
409             {record: recordIds, deleted: 'f'})
410         .subscribe(
411             part => {
412                 if (!this.bibParts[part.record()]) {
413                     this.bibParts[part.record()] = [];
414                 }
415                 this.bibParts[part.record()].push(part);
416             },
417             err => {},
418             () => {
419                 recordIds.forEach(bibId => {
420                     if (this.bibParts[bibId]) {
421                         this.bibParts[bibId] = this.bibParts[bibId]
422                         .sort((p1, p2) =>
423                             p1.label_sortkey() < p2.label_sortkey() ? -1 : 1);
424                     }
425                 });
426             }
427         );
428     }
429
430
431     copyStatIsMagic(statId: number): boolean {
432         return this.magicCopyStats.includes(statId);
433     }
434
435     restrictCopyDelete(statId: number): boolean {
436         return this.copyStatuses[statId] &&
437                this.copyStatuses[statId].restrict_copy_delete() === 't';
438     }
439 }
440