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';
16 /* Managing volcopy data */
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};
28 export class VolCopyService {
33 defaults: VolCopyDefaults = null;
34 copyStatuses: {[id: number]: IdlObject} = {};
35 bibParts: {[bibId: number]: IdlObject[]} = {};
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} = {};
41 // Track this here so it can survive route changes.
42 currentContext: VolCopyContext;
44 statCatEntryMap: {[id: number]: IdlObject} = {}; // entry id => entry
46 templateNames: ComboboxEntry[] = [];
49 commonData: {[key: string]: IdlObject[]} = {};
50 magicCopyStats: number[] = [];
52 hideVolOrgs: number[] = [];
54 // Currently spans from volcopy.component to vol-edit.component.
55 genBarcodesRequested: EventEmitter<void> = new EventEmitter<void>();
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
70 // Fetch the data that is always needed.
71 load(): Promise<any> {
73 if (this.commonData.acp_item_type_map) { return Promise.resolve(); }
75 this.localOrgs = this.org.fullPath(this.auth.user().ws_ou(), true);
77 this.hideVolOrgs = this.org.list()
78 .filter(o => !this.org.canHaveVolumes(o)).map(o => o.id());
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];
86 .then(_ => this.ingestCommonData())
88 // These will come up later -- prefetch.
89 .then(_ => this.serverStore.getItemBatch([
91 'eg.cat.volcopy.defaults',
92 'eg.cat.record.summary.collapse'
95 .then(_ => this.holdings.getMagicCopyStatuses())
96 .then(stats => this.magicCopyStats = stats)
97 .then(_ => this.fetchDefaults())
98 .then(_ => this.fetchTemplates());
103 this.commonData.acp_location.forEach(
104 loc => this.copyLocationMap[loc.id()] = loc);
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);
111 this.commonData.acn_suffix =
112 this.commonData.acn_suffix.filter(sfx => sfx.id() !== -1);
114 this.commonData.acp_status.forEach(
115 stat => this.copyStatuses[stat.id()] = stat);
117 this.commonData.acp_stat_cat.forEach(cat => {
118 cat.entries().forEach(
119 entry => this.statCatEntryMap[entry.id()] = entry);
123 getLocation(id: number): Promise<IdlObject> {
124 if (this.copyLocationMap[id]) {
125 return Promise.resolve(this.copyLocationMap[id]);
128 return this.pcrud.retrieve('acpl', id)
129 .pipe(tap(loc => this.copyLocationMap[loc.id()] = loc))
133 fetchTemplates(): Promise<any> {
135 return this.serverStore.getItem('cat.copy.templates')
138 if (!templates) { return null; }
140 this.templates = templates;
142 this.templateNames = Object.keys(templates)
143 .sort((n1, n2) => n1 < n2 ? -1 : 1)
144 .map(name => ({id: name, label: name}));
146 this.store.removeLocalItem('cat.copy.templates');
151 saveTemplates(): Promise<any> {
152 this.store.setLocalItem('cat.copy.templates', this.templates);
154 return this.fetchTemplates();
157 fetchDefaults(): Promise<any> {
158 if (this.defaults) { return Promise.resolve(); }
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 = {}; }
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([]); }
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.
180 return this.net.request(
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();
190 createStubVol(recordId: number, orgId: number, options?: any): IdlObject {
191 if (!options) { options = {}; }
193 const vol = this.idl.create('acn');
194 vol.id(this.autoId--);
196 vol.record(recordId);
198 vol.owning_lib(Number(orgId));
199 vol.prefix(this.defaults.values.prefix || -1);
200 vol.suffix(this.defaults.values.suffix || -1);
205 createStubCopy(vol: IdlObject, options?: any): IdlObject {
206 if (!options) { options = {}; }
208 const copy = this.idl.create('acp');
209 copy.id(this.autoId--);
211 copy.call_number(vol); // fleshed
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()));
222 copy.opac_visible('t');
224 copy.mint_condition('t');
229 copy.stat_cat_entries([]);
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> {
239 return this.applyVolClasses(vols)
240 .then(_ => this.applyVolLabels(vols));
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> {
248 vols = vols.filter(v => !v.label_class());
250 const orgIds: any = {};
251 vols.forEach(vol => orgIds[vol.owning_lib()] = true);
253 let promise = Promise.resolve(); // Serialization
255 if (this.defaults.values.classification) {
256 // Workstation default classification overrides the
257 // classification that might be used at the owning lib.
260 vol.label_class(this.defaults.values.classification));
266 // Get the label class default for each owning lib and
267 // apply to the volumes owned by that lib.
269 Object.keys(orgIds).map(orgId => Number(orgId))
271 promise = promise.then(_ => {
273 return this.org.settings(
274 'cat.default_classification_scheme', orgId)
277 const orgVols = vols.filter(v => v.owning_lib() === orgId);
278 orgVols.forEach(vol => {
280 sets['cat.default_classification_scheme'] || 1
291 // Apply labels to volumes based on the appropriate MARC call number.
292 applyVolLabels(vols: IdlObject[]): Promise<any> {
294 vols = vols.filter(v => !v.label());
297 let promise = Promise.resolve();
299 vols.forEach(vol => {
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; }
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; }
311 promise = promise.then(_ => {
312 return this.net.request(
314 'open-ils.cat.biblio.record.marc_cn.retrieve',
315 vol.record(), vol.label_class()).toPromise()
318 // Use '_' as a placeholder to indicate when a
319 // vol has already been addressed.
322 if (cnList.length > 0) {
323 const field = Object.keys(cnList[0])[0];
324 label = cnList[0][field];
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()) {
339 return promise.then(_ => {
340 // Remove the placeholder label
341 vols.forEach(vol => {
342 if (vol.label() === '_') { vol.label(''); }
347 // Sets the default copy status for a batch of copies.
348 setCopyStatus(copies: IdlObject[]): Promise<any> {
350 const fastAdd = this.currentContext.fastAdd;
352 const setting = fastAdd ?
353 'cat.default_copy_status_fast' :
354 'cat.default_copy_status_normal';
356 const orgs: any = {};
357 copies.forEach(copy => orgs[copy.circ_lib()] = 1);
359 let promise = Promise.resolve(); // Seralize
361 // Pre-fetch needed org settings
362 Object.keys(orgs).forEach(org => {
363 promise = promise.then(_ => {
364 return this.org.settings(setting, +org)
366 orgs[org] = sets[setting] || (fastAdd ? 0 : 5);
372 Object.keys(orgs).forEach(org => {
373 copies.filter(copy => copy.circ_lib() === +org)
374 .forEach(copy => copy.status(orgs[org]));
381 saveDefaults(): Promise<any> {
383 // Scrub unnecessary content before storing.
385 Object.keys(this.defaults.values).forEach(field => {
386 if (this.defaults.values[field] === null) {
387 delete this.defaults.values[field];
391 Object.keys(this.defaults.hidden).forEach(field => {
392 if (this.defaults.hidden[field] !== true) {
393 delete this.defaults.hidden[field];
397 return this.serverStore.setItem(
398 'eg.cat.volcopy.defaults', this.defaults);
401 fetchBibParts(recordIds: number[]) {
403 if (recordIds.length === 0) { return; }
405 // All calls fetch updated data since we may be creating
406 // new mono parts during editing.
408 this.pcrud.search('bmp',
409 {record: recordIds, deleted: 'f'})
412 if (!this.bibParts[part.record()]) {
413 this.bibParts[part.record()] = [];
415 this.bibParts[part.record()].push(part);
419 recordIds.forEach(bibId => {
420 if (this.bibParts[bibId]) {
421 this.bibParts[bibId] = this.bibParts[bibId]
423 p1.label_sortkey() < p2.label_sortkey() ? -1 : 1);
431 copyStatIsMagic(statId: number): boolean {
432 return this.magicCopyStats.includes(statId);
435 restrictCopyDelete(statId: number): boolean {
436 return this.copyStatuses[statId] &&
437 this.copyStatuses[statId].restrict_copy_delete() === 't';