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';
15 /* Managing volcopy data */
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};
27 export class VolCopyService {
32 defaults: VolCopyDefaults = null;
33 copyStatuses: {[id: number]: IdlObject} = {};
34 bibParts: {[bibId: number]: IdlObject[]} = {};
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} = {};
40 // Track this here so it can survive route changes.
41 currentContext: VolCopyContext;
43 statCatEntryMap: {[id: number]: IdlObject} = {}; // entry id => entry
45 templateNames: ComboboxEntry[] = [];
48 commonData: {[key: string]: IdlObject[]} = {};
49 magicCopyStats: number[] = [];
51 hideVolOrgs: number[] = [];
53 // Currently spans from volcopy.component to vol-edit.component.
54 genBarcodesRequested: EventEmitter<void> = new EventEmitter<void>();
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
69 // Fetch the data that is always needed.
70 load(): Promise<any> {
72 if (this.commonData.acp_item_type_map) { return Promise.resolve(); }
74 this.localOrgs = this.org.fullPath(this.auth.user().ws_ou(), true);
76 this.hideVolOrgs = this.org.list()
77 .filter(o => !this.org.canHaveVolumes(o)).map(o => o.id());
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];
85 .then(_ => this.ingestCommonData())
87 // These will come up later -- prefetch.
88 .then(_ => this.serverStore.getItemBatch([
90 'eg.cat.volcopy.defaults',
91 'eg.cat.record.summary.collapse'
94 .then(_ => this.holdings.getMagicCopyStatuses())
95 .then(stats => this.magicCopyStats = stats)
96 .then(_ => this.fetchDefaults())
97 .then(_ => this.fetchTemplates());
102 this.commonData.acp_location.forEach(
103 loc => this.copyLocationMap[loc.id()] = loc);
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);
110 this.commonData.acn_suffix =
111 this.commonData.acn_suffix.filter(sfx => sfx.id() !== -1);
113 this.commonData.acp_status.forEach(
114 stat => this.copyStatuses[stat.id()] = stat);
116 this.commonData.acp_stat_cat.forEach(cat => {
117 cat.entries().forEach(
118 entry => this.statCatEntryMap[entry.id()] = entry);
122 getLocation(id: number): Promise<IdlObject> {
123 if (this.copyLocationMap[id]) {
124 return Promise.resolve(this.copyLocationMap[id]);
127 return this.pcrud.retrieve('acpl', id)
128 .pipe(tap(loc => this.copyLocationMap[loc.id()] = loc))
132 fetchTemplates(): Promise<any> {
134 return this.serverStore.getItem('cat.copy.templates')
137 if (!templates) { return null; }
139 this.templates = templates;
141 this.templateNames = Object.keys(templates)
142 .sort((n1, n2) => n1 < n2 ? -1 : 1)
143 .map(name => ({id: name, label: name}));
145 this.store.removeLocalItem('cat.copy.templates');
150 saveTemplates(): Promise<any> {
151 return this.serverStore.setItem('cat.copy.templates', this.templates)
152 .then(() => this.fetchTemplates());
155 fetchDefaults(): Promise<any> {
156 if (this.defaults) { return Promise.resolve(); }
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 = {}; }
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([]); }
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.
178 return this.net.request(
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();
188 createStubVol(recordId: number, orgId: number, options?: any): IdlObject {
189 if (!options) { options = {}; }
191 const vol = this.idl.create('acn');
192 vol.id(this.autoId--);
194 vol.record(recordId);
196 vol.owning_lib(Number(orgId));
197 vol.prefix(this.defaults.values.prefix || -1);
198 vol.suffix(this.defaults.values.suffix || -1);
203 createStubCopy(vol: IdlObject, options?: any): IdlObject {
204 if (!options) { options = {}; }
206 const copy = this.idl.create('acp');
207 copy.id(this.autoId--);
209 copy.call_number(vol); // fleshed
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()));
220 copy.opac_visible('t');
222 copy.mint_condition('t');
227 copy.copy_alerts([]);
228 copy.stat_cat_entries([]);
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> {
238 return this.applyVolClasses(vols)
239 .then(_ => this.applyVolLabels(vols));
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> {
247 vols = vols.filter(v => !v.label_class());
249 const orgIds: any = {};
250 vols.forEach(vol => orgIds[vol.owning_lib()] = true);
252 let promise = Promise.resolve(); // Serialization
254 if (this.defaults.values.classification) {
255 // Workstation default classification overrides the
256 // classification that might be used at the owning lib.
259 vol.label_class(this.defaults.values.classification));
265 // Get the label class default for each owning lib and
266 // apply to the volumes owned by that lib.
268 Object.keys(orgIds).map(orgId => Number(orgId))
270 promise = promise.then(_ => {
272 return this.org.settings(
273 'cat.default_classification_scheme', orgId)
276 const orgVols = vols.filter(v => v.owning_lib() === orgId);
277 orgVols.forEach(vol => {
279 sets['cat.default_classification_scheme'] || 1
290 // Apply labels to volumes based on the appropriate MARC call number.
291 applyVolLabels(vols: IdlObject[]): Promise<any> {
293 vols = vols.filter(v => !v.label());
296 let promise = Promise.resolve();
298 vols.forEach(vol => {
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; }
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; }
310 promise = promise.then(_ => {
311 return this.net.request(
313 'open-ils.cat.biblio.record.marc_cn.retrieve',
314 vol.record(), vol.label_class()).toPromise()
317 // Use '_' as a placeholder to indicate when a
318 // vol has already been addressed.
321 if (cnList.length > 0) {
322 const field = Object.keys(cnList[0])[0];
323 label = cnList[0][field];
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()) {
338 return promise.then(_ => {
339 // Remove the placeholder label
340 vols.forEach(vol => {
341 if (vol.label() === '_') { vol.label(''); }
346 // Sets the default copy status for a batch of copies.
347 setCopyStatus(copies: IdlObject[]): Promise<any> {
349 const fastAdd = this.currentContext.fastAdd;
351 const setting = fastAdd ?
352 'cat.default_copy_status_fast' :
353 'cat.default_copy_status_normal';
355 const orgs: any = {};
356 copies.forEach(copy => orgs[copy.circ_lib()] = 1);
358 let promise = Promise.resolve(); // Seralize
360 // Pre-fetch needed org settings
361 Object.keys(orgs).forEach(org => {
362 promise = promise.then(_ => {
363 return this.org.settings(setting, +org)
365 orgs[org] = sets[setting] || (fastAdd ? 0 : 5);
371 Object.keys(orgs).forEach(org => {
372 copies.filter(copy => copy.circ_lib() === +org)
373 .forEach(copy => copy.status(orgs[org]));
380 saveDefaults(): Promise<any> {
382 // Scrub unnecessary content before storing.
384 Object.keys(this.defaults.values).forEach(field => {
385 if (this.defaults.values[field] === null) {
386 delete this.defaults.values[field];
390 Object.keys(this.defaults.hidden).forEach(field => {
391 if (this.defaults.hidden[field] !== true) {
392 delete this.defaults.hidden[field];
396 return this.serverStore.setItem(
397 'eg.cat.volcopy.defaults', this.defaults);
400 fetchBibParts(recordIds: number[]) {
402 if (recordIds.length === 0) { return; }
404 // All calls fetch updated data since we may be creating
405 // new mono parts during editing.
407 this.pcrud.search('bmp',
408 {record: recordIds, deleted: 'f'})
411 if (!this.bibParts[part.record()]) {
412 this.bibParts[part.record()] = [];
414 this.bibParts[part.record()].push(part);
418 recordIds.forEach(bibId => {
419 if (this.bibParts[bibId]) {
420 this.bibParts[bibId] = this.bibParts[bibId]
422 p1.label_sortkey() < p2.label_sortkey() ? -1 : 1);
430 copyStatIsMagic(statId: number): boolean {
431 return this.magicCopyStats.includes(statId);
434 restrictCopyDelete(statId: number): boolean {
435 return this.copyStatuses[statId] &&
436 this.copyStatuses[statId].restrict_copy_delete() === 't';
439 // Returns true if any items are missing values for a required stat cat.
440 missingRequiredStatCat(): boolean {
443 this.currentContext.copyList().forEach(copy => {
444 if (!copy.barcode()) { return; }
446 this.commonData.acp_stat_cat.forEach(cat => {
447 if (cat.required() !== 't') { return; }
449 const matches = copy.stat_cat_entries()
450 .filter(e => e.stat_cat() === cat.id());
452 if (matches.length === 0) {