1 import {Component, OnInit, AfterViewInit, ViewChild, Renderer2} from '@angular/core';
2 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
3 import {tap} from 'rxjs/operators';
4 import {IdlObject, IdlService} from '@eg/core/idl.service';
5 import {EventService} from '@eg/core/event.service';
6 import {OrgService} from '@eg/core/org.service';
7 import {NetService} from '@eg/core/net.service';
8 import {AuthService} from '@eg/core/auth.service';
9 import {PcrudService} from '@eg/core/pcrud.service';
10 import {HoldingsService, CallNumData} from '@eg/staff/share/holdings/holdings.service';
11 import {VolCopyContext} from './volcopy';
12 import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
13 import {AnonCacheService} from '@eg/share/util/anon-cache.service';
14 import {VolCopyService} from './volcopy.service';
15 import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
21 'call_number', 'location', 'parts', 'tags',
22 'creator', 'editor', 'stat_cat_entries'
29 interface EditSession {
31 // Unset if editing in multi-record mode
37 // Adding to or creating new call numbers
40 // Hide the volumes editor
43 // Hide the copy attrs editor.
48 templateUrl: 'volcopy.component.html'
50 export class VolCopyComponent implements OnInit {
52 context: VolCopyContext;
54 sessionExpired = false;
57 tab = 'holdings'; // holdings | attrs | config
58 target: string; // item | callnumber | record | session
59 targetId: string; // id value or session string
65 private router: Router,
66 private route: ActivatedRoute,
67 private renderer: Renderer2,
68 private evt: EventService,
69 private idl: IdlService,
70 private org: OrgService,
71 private net: NetService,
72 private auth: AuthService,
73 private pcrud: PcrudService,
74 private cache: AnonCacheService,
75 private holdings: HoldingsService,
76 private volcopy: VolCopyService
80 this.route.paramMap.subscribe(
81 (params: ParamMap) => this.negotiateRoute(params));
84 negotiateRoute(params: ParamMap) {
85 this.tab = params.get('tab') || 'holdings';
86 this.target = params.get('target');
87 this.targetId = params.get('target_id');
89 if (this.volcopy.currentContext) {
90 // Avoid clobbering the context on route change.
91 this.context = this.volcopy.currentContext;
93 this.context = new VolCopyContext();
94 this.context.org = this.org; // inject;
97 switch (this.target) {
99 this.context.copyId = +this.targetId;
102 this.context.volId = +this.targetId;
105 this.context.recordId = +this.targetId;
108 this.context.session = this.targetId;
112 if (this.volcopy.currentContext) {
113 this.loading = false;
116 // Avoid refetching the data during route changes.
117 this.volcopy.currentContext = this.context;
122 load(copyIds?: number[]): Promise<any> {
123 this.sessionExpired = false;
125 this.context.reset();
127 return this.volcopy.load()
128 .then(_ => this.fetchHoldings(copyIds))
129 .then(_ => this.volcopy.applyVolLabels(
130 this.context.volNodes().map(n => n.target)))
131 .then(_ => this.context.sortHoldings())
132 .then(_ => this.context.setRecordId())
133 .then(_ => this.printLabels =
134 this.volcopy.defaults.values.print_labels === true)
136 // unified display has no 'attrs' tab
137 if (this.volcopy.defaults.values.unified_display
138 && this.tab === 'attrs') {
139 this.tab = 'holdings';
143 .then(_ => this.loading = false);
146 fetchHoldings(copyIds?: number[]): Promise<any> {
148 if (copyIds && copyIds.length > 0) {
149 // Reloading copies that were just edited.
150 return this.fetchCopies(copyIds);
152 } else if (this.context.session) {
153 this.context.sessionType = 'mixed';
154 return this.fetchSession(this.context.session);
156 } else if (this.context.copyId) {
157 this.context.sessionType = 'copy';
158 return this.fetchCopies(this.context.copyId);
160 } else if (this.context.volId) {
161 this.context.sessionType = 'vol';
162 return this.fetchVols(this.context.volId);
164 } else if (this.context.recordId) {
165 this.context.sessionType = 'record';
166 return this.fetchRecords(this.context.recordId);
170 // Changing a tab in the UI means changing the route.
171 // Changing the route ultimately results in changing the tab.
172 beforeTabChange(evt: NgbNavChangeEvent) {
173 evt.preventDefault();
174 this.tab = evt.nextId;
180 `/staff/cat/volcopy/${this.tab}/${this.target}/${this.targetId}`;
182 // Retain search parameters
183 this.router.navigate([url], {queryParamsHandling: 'merge'});
186 fetchSession(session: string): Promise<any> {
188 return this.cache.getItem(session, 'edit-these-copies')
189 .then((editSession: EditSession) => {
192 this.loading = false;
193 this.sessionExpired = true;
194 return Promise.reject('Session Expired');
197 console.debug('Edit Session', editSession);
199 this.context.recordId = editSession.record_id;
201 if (editSession.copies && editSession.copies.length > 0) {
202 return this.fetchCopies(editSession.copies);
205 const volsToFetch = [];
206 const volsToCreate = [];
207 editSession.raw.forEach((volData: CallNumData) => {
208 this.context.fastAdd = volData.fast_add === true;
210 if (volData.callnumber > 0) {
211 volsToFetch.push(volData);
213 volsToCreate.push(volData);
217 let promise = Promise.resolve();
218 if (volsToFetch.length > 0) {
219 promise = promise.then(_ =>
220 this.fetchVolsStubCopies(volsToFetch));
223 if (volsToCreate.length > 0) {
224 promise = promise.then(_ =>
225 this.createVolsStubCopies(volsToCreate));
232 // Creating new vols. Each gets a stub copy.
233 createVolsStubCopies(volDataList: CallNumData[]): Promise<any> {
236 volDataList.forEach(volData => {
238 const vol = this.volcopy.createStubVol(
239 this.context.recordId,
240 volData.owner || this.auth.user().ws_ou()
243 if (volData.label) {vol.label(volData.label); }
245 volData.callnumber = vol.id(); // wanted by addStubCopies
247 this.context.findOrCreateVolNode(vol);
250 return this.addStubCopies(vols, volDataList)
251 .then(_ => this.volcopy.setVolClassLabels(vols));
254 // Fetch vols by ID, but instead of retrieving their copies
255 // add a stub copy to each.
256 fetchVolsStubCopies(volDataList: CallNumData[]): Promise<any> {
258 const volIds = volDataList.map(volData => volData.callnumber);
261 return this.pcrud.search('acn', {id: volIds})
262 .pipe(tap((vol: IdlObject) => vols.push(vol))).toPromise()
263 .then(_ => this.addStubCopies(vols, volDataList));
266 // Add a stub copy to each vol using data from the edit session.
267 addStubCopies(vols: IdlObject[], volDataList: CallNumData[]): Promise<any> {
270 vols.forEach(vol => {
271 const volData = volDataList.filter(
272 vData => vData.callnumber === vol.id())[0];
275 this.volcopy.createStubCopy(vol, {circLib: volData.owner});
277 this.context.findOrCreateCopyNode(copy);
281 return this.volcopy.setCopyStatus(copies, this.context.fastAdd);
284 fetchCopies(copyIds: number | number[]): Promise<any> {
285 const ids = [].concat(copyIds);
286 return this.pcrud.search('acp', {id: ids}, COPY_FLESH)
287 .pipe(tap(copy => this.context.findOrCreateCopyNode(copy)))
291 // Fetch call numbers and linked copies by call number ids.
292 fetchVols(volIds?: number | number[]): Promise<any> {
293 const ids = [].concat(volIds);
295 return this.pcrud.search('acn', {id: ids})
296 .pipe(tap(vol => this.context.findOrCreateVolNode(vol)))
297 .toPromise().then(_ => {
298 return this.pcrud.search('acp',
299 {call_number: ids, deleted: 'f'}, COPY_FLESH
300 ).pipe(tap(copy => this.context.findOrCreateCopyNode(copy))
305 // Fetch call numbers and copies by record ids.
306 fetchRecords(recordIds: number | number[]): Promise<any> {
307 const ids = [].concat(recordIds);
309 return this.pcrud.search('acn',
310 {record: ids, deleted: 'f', label: {'!=' : '##URI##'}},
311 {}, {idlist: true, atomic: true}
312 ).toPromise().then(volIds => this.fetchVols(volIds));
316 save(close?: boolean): Promise<any> {
319 // Volume update API wants volumes fleshed with copies, instead
320 // of the other way around, which is what we have here.
321 const volumes: IdlObject[] = [];
323 this.context.volNodes().forEach(volNode => {
324 const newVol = this.idl.clone(volNode.target);
325 const copies: IdlObject[] = [];
327 volNode.children.forEach(copyNode => {
328 const copy = copyNode.target;
330 if (copy.isnew() && !copy.barcode()) {
331 // A new copy w/ no barcode is a stub copy sitting
332 // on an empty call number. Ignore it.
336 if (copy.ischanged() || copy.isnew() || copy.isdeleted()) {
337 const copyClone = this.idl.clone(copy);
338 // De-flesh call number
339 copyClone.call_number(copy.call_number().id());
340 copies.push(copyClone);
344 newVol.copies(copies);
346 if (newVol.ischanged() || newVol.isnew() || copies.length > 0) {
347 volumes.push(newVol);
351 this.context.volsToDelete.forEach(vol => {
352 const cloneVol = this.idl.clone(vol);
353 // No need to flesh copies -- they'll be force deleted.
355 volumes.push(cloneVol);
358 this.context.copiesToDelete.forEach(copy => {
359 const cloneCopy = this.idl.clone(copy);
360 const copyVol = cloneCopy.call_number();
361 cloneCopy.call_number(copyVol.id()); // de-flesh
363 let vol = volumes.filter(v => v.id() === copyVol.id())[0];
366 vol.copies().push(cloneCopy);
368 vol = this.idl.clone(copyVol);
369 vol.copies([cloneCopy]);
375 // De-flesh before posting
376 volumes.forEach(vol => {
377 vol.copies().forEach(copy => {
378 ['editor', 'creator', 'location'].forEach(field => {
379 if (typeof copy[field]() === 'object') {
380 copy[field](copy[field]().id());
386 let promise: Promise<number[]> = Promise.resolve([]);
388 if (volumes.length > 0) {
389 promise = this.saveApi(volumes, false, close);
392 return promise.then(copyIds => {
394 // In addition to the copies edited in this update call,
395 // reload any other copies that were previously loaded.
396 const ids: any = {}; // dedupe
397 this.context.copyList()
399 .filter(id => id > 0) // scrub the new copy IDs
401 .forEach(id => ids[id] = true);
403 copyIds = Object.keys(ids).map(id => Number(id));
406 return this.openPrintLabels(copyIds)
407 .then(_ => setTimeout(() => window.close()));
410 return this.load(Object.keys(ids).map(id => Number(id)));
412 }).then(_ => this.loading = false);
415 saveApi(volumes: IdlObject[], override?:
416 boolean, close?: boolean): Promise<number[]> {
418 let method = 'open-ils.cat.asset.volume.fleshed.batch.update';
419 if (override) { method += '.override'; }
421 return this.net.request('open-ils.cat',
422 method, this.auth.token(), volumes, true,
423 { auto_merge_vols: true,
425 return_copy_ids: true,
426 force_delete_copies: true
429 ).toPromise().then(copyIds => {
431 const evt = this.evt.parse(copyIds);
434 // TODO: handle overrides?
435 // return this.saveApi(volumes, true, close);
436 this.loading = false;
438 return Promise.reject();
446 this.volcopy.defaults.values.print_labels = this.printLabels === true;
447 this.volcopy.saveDefaults();
450 openPrintLabels(copyIds?: number[]): Promise<any> {
451 if (!this.printLabels) { return Promise.resolve(); }
453 if (!copyIds || copyIds.length === 0) {
454 copyIds = this.context.copyList()
455 .map(c => c.id()).filter(id => id > 0);
458 return this.net.request(
460 'open-ils.actor.anon_cache.set_value',
461 null, 'print-labels-these-copies', {copies : copyIds}
463 ).toPromise().then(key => {
465 const url = '/eg/staff/cat/printlabels/' + key;
466 setTimeout(() => window.open(url, '_blank'));
470 isNotSaveable(): boolean {
471 return !(this.volsCanSave && this.attrsCanSave);