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