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