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';
16 /* Managing volcopy data */
18 interface VolCopyDefaults {
19 values: {[field: string]: any};
20 hidden: {[field: string]: boolean};
24 export class VolCopyService {
29 defaults: VolCopyDefaults = null;
30 copyStatuses: {[id: number]: IdlObject} = {};
31 bibParts: {[bibId: number]: IdlObject[]} = {};
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} = {};
37 // Track this here so it can survive route changes.
38 currentContext: VolCopyContext;
40 statCatEntryMap: {[id: number]: IdlObject} = {}; // entry id => entry
42 templateNames: ComboboxEntry[] = [];
45 commonData: {[key: string]: IdlObject[]} = {};
46 magicCopyStats: number[] = [];
48 hideVolOrgs: number[] = [];
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
63 // Fetch the data that is always needed.
64 load(): Promise<any> {
66 if (this.commonData.acp_item_type_map) { return Promise.resolve(); }
68 this.localOrgs = this.org.fullPath(this.auth.user().ws_ou(), true);
70 this.hideVolOrgs = this.org.list()
71 .filter(o => !this.org.canHaveVolumes(o)).map(o => o.id());
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];
79 .then(_ => this.ingestCommonData())
81 // These will come up later -- prefetch.
82 .then(_ => this.serverStore.getItemBatch([
84 'eg.cat.volcopy.defaults',
85 'eg.cat.record.summary.collapse'
88 .then(_ => this.holdings.getMagicCopyStatuses())
89 .then(stats => this.magicCopyStats = stats)
90 .then(_ => this.fetchDefaults())
91 .then(_ => this.fetchTemplates());
96 this.commonData.acp_location.forEach(
97 loc => this.copyLocationMap[loc.id()] = loc);
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);
104 this.commonData.acn_suffix =
105 this.commonData.acn_suffix.filter(sfx => sfx.id() !== -1);
107 this.commonData.acp_status.forEach(
108 stat => this.copyStatuses[stat.id()] = stat);
110 this.commonData.acp_stat_cat.forEach(cat => {
111 cat.entries().forEach(
112 entry => this.statCatEntryMap[entry.id()] = entry);
116 getLocation(id: number): Promise<IdlObject> {
117 if (this.copyLocationMap[id]) {
118 return Promise.resolve(this.copyLocationMap[id]);
121 return this.pcrud.retrieve('acpl', id)
122 .pipe(tap(loc => this.copyLocationMap[loc.id()] = loc))
126 fetchTemplates(): Promise<any> {
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');
132 const promise = tmpls ?
133 this.serverStore.setItem('cat.copy.templates', tmpls) :
137 .then(_ => this.serverStore.getItem('cat.copy.templates'))
140 if (!templates) { return null; }
142 this.templates = templates;
144 this.templateNames = Object.keys(templates)
145 .sort((n1, n2) => n1 < n2 ? -1 : 1)
146 .map(name => ({id: name, label: name}));
148 this.store.removeLocalItem('cat.copy.templates');
153 saveTemplates(): Promise<any> {
154 this.store.setLocalItem('cat.copy.templates', this.templates);
156 return this.fetchTemplates();
159 fetchDefaults(): Promise<any> {
160 if (this.defaults) { return Promise.resolve(); }
162 return this.serverStore.getItem('eg.cat.volcopy.defaults').then(
163 (defaults: VolCopyDefaults) => {
164 this.defaults = defaults || {values: {}, hidden: {}};
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([]); }
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.
179 return this.net.request(
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();
189 createStubVol(recordId: number, orgId: number, options?: any): IdlObject {
190 if (!options) { options = {}; }
192 const vol = this.idl.create('acn');
193 vol.id(this.autoId--);
195 vol.record(recordId);
197 vol.owning_lib(Number(orgId));
198 vol.prefix(this.defaults.values.prefix || -1);
199 vol.suffix(this.defaults.values.suffix || -1);
204 createStubCopy(vol: IdlObject, options?: any): IdlObject {
205 if (!options) { options = {}; }
207 const copy = this.idl.create('acp');
208 copy.id(this.autoId--);
210 copy.call_number(vol); // fleshed
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()));
221 copy.opac_visible('t');
223 copy.mint_condition('t');
227 copy.stat_cat_entries([]);
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> {
237 return this.applyVolClasses(vols)
238 .then(_ => this.applyVolLabels(vols));
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> {
246 vols = vols.filter(v => !v.label_class());
248 const orgIds: any = {};
249 vols.forEach(vol => orgIds[vol.owning_lib()] = true);
251 let promise = Promise.resolve(); // Serialization
253 if (this.defaults.values.classification) {
254 // Workstation default classification overrides the
255 // classification that might be used at the owning lib.
258 vol.label_class(this.defaults.values.classification));
264 // Get the label class default for each owning lib and
265 // apply to the volumes owned by that lib.
267 Object.keys(orgIds).map(orgId => Number(orgId))
269 promise = promise.then(_ => {
271 return this.org.settings(
272 'cat.default_classification_scheme', orgId)
275 const orgVols = vols.filter(v => v.owning_lib() === orgId);
276 orgVols.forEach(vol => {
278 sets['cat.default_classification_scheme'] || 1
289 // Apply labels to volumes based on the appropriate MARC call number.
290 applyVolLabels(vols: IdlObject[]): Promise<any> {
292 vols = vols.filter(v => !v.label());
295 let promise = Promise.resolve();
297 vols.forEach(vol => {
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; }
304 promise = promise.then(_ => {
305 return this.net.request(
307 'open-ils.cat.biblio.record.marc_cn.retrieve',
308 vol.record(), vol.label_class()).toPromise()
311 // Use '_' as a placeholder to indicate when a
312 // vol has already been addressed.
315 if (cnList.length > 0) {
316 const field = Object.keys(cnList[0])[0];
317 label = cnList[0][field];
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()) {
332 return promise.then(_ => {
333 // Remove the placeholder label
334 vols.forEach(vol => {
335 if (vol.label() === '_') { vol.label(''); }
340 // Sets the default copy status for a batch of copies.
341 setCopyStatus(copies: IdlObject[], fastAdd: boolean): Promise<any> {
343 const setting = fastAdd ?
344 'cat.default_copy_status_fast' :
345 'cat.default_copy_status_normal';
347 let promise = Promise.resolve(); // Seralize
349 copies.forEach(copy => {
351 // Avoid unnecessary lookups. Copy may have been modified
352 // during a previous iteration of this loop.
353 if (!isNaN(copy.status())) { return; }
355 promise = promise.then(_ =>
356 this.org.settings(setting, copy.circ_lib())
360 // 0 == Available; 5 == In Process
361 const stat = sets[setting] || (fastAdd ? 0 : 5);
363 copies.forEach(copy2 => {
364 if (copy2.circ_lib() === copy.circ_lib()) {
375 saveDefaults(): Promise<any> {
377 // Scrub unnecessary content before storing.
379 Object.keys(this.defaults.values).forEach(field => {
380 if (this.defaults.values[field] === null) {
381 delete this.defaults.values[field];
385 Object.keys(this.defaults.hidden).forEach(field => {
386 if (this.defaults.hidden[field] !== true) {
387 delete this.defaults.hidden[field];
391 return this.serverStore.setItem(
392 'eg.cat.volcopy.defaults', this.defaults);
395 fetchBibParts(recordIds: number[]) {
397 if (recordIds.length === 0) { return; }
400 if (this.bibParts[recordIds[0]]) { return; }
402 this.pcrud.search('bmp',
403 {record: recordIds, deleted: 'f'})
406 if (!this.bibParts[part.record()]) {
407 this.bibParts[part.record()] = [];
409 this.bibParts[part.record()].push(part);
413 recordIds.forEach(bibId => {
414 if (this.bibParts[bibId]) {
415 this.bibParts[bibId] = this.bibParts[bibId]
417 p1.label_sortkey() < p2.label_sortkey() ? -1 : 1);
425 copyStatIsMagic(statId: number): boolean {
426 return this.magicCopyStats.includes(statId);
429 restrictCopyDelete(statId: number): boolean {
430 return this.copyStatuses[statId] &&
431 this.copyStatuses[statId].restrict_copy_delete() === 't';