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 // 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');
139 const promise = tmpls ?
140 this.serverStore.setItem('cat.copy.templates', tmpls) :
144 .then(_ => this.serverStore.getItem('cat.copy.templates'))
147 if (!templates) { return null; }
149 this.templates = templates;
151 this.templateNames = Object.keys(templates)
152 .sort((n1, n2) => n1 < n2 ? -1 : 1)
153 .map(name => ({id: name, label: name}));
155 this.store.removeLocalItem('cat.copy.templates');
160 saveTemplates(): Promise<any> {
161 this.store.setLocalItem('cat.copy.templates', this.templates);
163 return this.fetchTemplates();
166 fetchDefaults(): Promise<any> {
167 if (this.defaults) { return Promise.resolve(); }
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 = {}; }
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([]); }
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.
189 return this.net.request(
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();
199 createStubVol(recordId: number, orgId: number, options?: any): IdlObject {
200 if (!options) { options = {}; }
202 const vol = this.idl.create('acn');
203 vol.id(this.autoId--);
205 vol.record(recordId);
207 vol.owning_lib(Number(orgId));
208 vol.prefix(this.defaults.values.prefix || -1);
209 vol.suffix(this.defaults.values.suffix || -1);
214 createStubCopy(vol: IdlObject, options?: any): IdlObject {
215 if (!options) { options = {}; }
217 const copy = this.idl.create('acp');
218 copy.id(this.autoId--);
220 copy.call_number(vol); // fleshed
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()));
231 copy.opac_visible('t');
233 copy.mint_condition('t');
238 copy.stat_cat_entries([]);
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> {
248 return this.applyVolClasses(vols)
249 .then(_ => this.applyVolLabels(vols));
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> {
257 vols = vols.filter(v => !v.label_class());
259 const orgIds: any = {};
260 vols.forEach(vol => orgIds[vol.owning_lib()] = true);
262 let promise = Promise.resolve(); // Serialization
264 if (this.defaults.values.classification) {
265 // Workstation default classification overrides the
266 // classification that might be used at the owning lib.
269 vol.label_class(this.defaults.values.classification));
275 // Get the label class default for each owning lib and
276 // apply to the volumes owned by that lib.
278 Object.keys(orgIds).map(orgId => Number(orgId))
280 promise = promise.then(_ => {
282 return this.org.settings(
283 'cat.default_classification_scheme', orgId)
286 const orgVols = vols.filter(v => v.owning_lib() === orgId);
287 orgVols.forEach(vol => {
289 sets['cat.default_classification_scheme'] || 1
300 // Apply labels to volumes based on the appropriate MARC call number.
301 applyVolLabels(vols: IdlObject[]): Promise<any> {
303 vols = vols.filter(v => !v.label());
306 let promise = Promise.resolve();
308 vols.forEach(vol => {
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; }
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; }
320 promise = promise.then(_ => {
321 return this.net.request(
323 'open-ils.cat.biblio.record.marc_cn.retrieve',
324 vol.record(), vol.label_class()).toPromise()
327 // Use '_' as a placeholder to indicate when a
328 // vol has already been addressed.
331 if (cnList.length > 0) {
332 const field = Object.keys(cnList[0])[0];
333 label = cnList[0][field];
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()) {
348 return promise.then(_ => {
349 // Remove the placeholder label
350 vols.forEach(vol => {
351 if (vol.label() === '_') { vol.label(''); }
356 // Sets the default copy status for a batch of copies.
357 setCopyStatus(copies: IdlObject[]): Promise<any> {
359 const fastAdd = this.currentContext.fastAdd;
361 const setting = fastAdd ?
362 'cat.default_copy_status_fast' :
363 'cat.default_copy_status_normal';
365 const orgs: any = {};
366 copies.forEach(copy => orgs[copy.circ_lib()] = 1);
368 let promise = Promise.resolve(); // Seralize
370 // Pre-fetch needed org settings
371 Object.keys(orgs).forEach(org => {
372 promise = promise.then(_ => {
373 return this.org.settings(setting, +org)
375 orgs[org] = sets[setting] || (fastAdd ? 0 : 5);
381 Object.keys(orgs).forEach(org => {
382 copies.filter(copy => copy.circ_lib() === +org)
383 .forEach(copy => copy.status(orgs[org]));
390 saveDefaults(): Promise<any> {
392 // Scrub unnecessary content before storing.
394 Object.keys(this.defaults.values).forEach(field => {
395 if (this.defaults.values[field] === null) {
396 delete this.defaults.values[field];
400 Object.keys(this.defaults.hidden).forEach(field => {
401 if (this.defaults.hidden[field] !== true) {
402 delete this.defaults.hidden[field];
406 return this.serverStore.setItem(
407 'eg.cat.volcopy.defaults', this.defaults);
410 fetchBibParts(recordIds: number[]) {
412 if (recordIds.length === 0) { return; }
414 // All calls fetch updated data since we may be creating
415 // new mono parts during editing.
417 this.pcrud.search('bmp',
418 {record: recordIds, deleted: 'f'})
421 if (!this.bibParts[part.record()]) {
422 this.bibParts[part.record()] = [];
424 this.bibParts[part.record()].push(part);
428 recordIds.forEach(bibId => {
429 if (this.bibParts[bibId]) {
430 this.bibParts[bibId] = this.bibParts[bibId]
432 p1.label_sortkey() < p2.label_sortkey() ? -1 : 1);
440 copyStatIsMagic(statId: number): boolean {
441 return this.magicCopyStats.includes(statId);
444 restrictCopyDelete(statId: number): boolean {
445 return this.copyStatuses[statId] &&
446 this.copyStatuses[statId].restrict_copy_delete() === 't';