1 import {Component, OnInit, AfterViewInit, ViewChild, HostListener} 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 {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
14 import {AnonCacheService} from '@eg/share/util/anon-cache.service';
15 import {VolCopyService} from './volcopy.service';
16 import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
17 import {BroadcastService} from '@eg/share/util/broadcast.service';
23 'call_number', 'location', 'parts', 'tags',
24 'creator', 'editor', 'stat_cat_entries', 'notes'
31 interface EditSession {
33 // Unset if editing in multi-record mode
39 // Adding to or creating new call numbers
42 // Hide the volumes editor
45 // Hide the copy attrs editor.
50 templateUrl: 'volcopy.component.html'
52 export class VolCopyComponent implements OnInit {
54 context: VolCopyContext;
56 sessionExpired = false;
58 tab = 'holdings'; // holdings | attrs | config
59 target: string; // item | callnumber | record | session
60 targetId: string; // id value or session string
64 changesPending = false;
66 @ViewChild('pendingChangesDialog', {static: false})
67 pendingChangesDialog: ConfirmDialogComponent;
70 private router: Router,
71 private route: ActivatedRoute,
72 private evt: EventService,
73 private idl: IdlService,
74 private org: OrgService,
75 private net: NetService,
76 private auth: AuthService,
77 private pcrud: PcrudService,
78 private cache: AnonCacheService,
79 private broadcaster: BroadcastService,
80 private holdings: HoldingsService,
81 private volcopy: VolCopyService
85 this.route.paramMap.subscribe(
86 (params: ParamMap) => this.negotiateRoute(params));
89 negotiateRoute(params: ParamMap) {
90 this.tab = params.get('tab') || 'holdings';
91 this.target = params.get('target');
92 this.targetId = params.get('target_id');
94 if (this.volcopy.currentContext) {
95 // Avoid clobbering the context on route change.
96 this.context = this.volcopy.currentContext;
98 this.context = new VolCopyContext();
99 this.context.org = this.org; // inject;
102 switch (this.target) {
104 this.context.copyId = +this.targetId;
107 this.context.volId = +this.targetId;
110 this.context.recordId = +this.targetId;
113 this.context.session = this.targetId;
117 if (this.volcopy.currentContext) {
118 this.loading = false;
121 // Avoid refetching the data during route changes.
122 this.volcopy.currentContext = this.context;
127 load(copyIds?: number[]): Promise<any> {
128 this.sessionExpired = false;
130 this.context.reset();
132 return this.volcopy.load()
133 .then(_ => this.fetchHoldings(copyIds))
134 .then(_ => this.volcopy.applyVolLabels(
135 this.context.volNodes().map(n => n.target)))
136 .then(_ => this.context.sortHoldings())
137 .then(_ => this.context.setRecordId())
139 // unified display has no 'attrs' tab
140 if (this.volcopy.defaults.values.unified_display
141 && this.tab === 'attrs') {
142 this.tab = 'holdings';
146 .then(_ => this.loading = false);
149 fetchHoldings(copyIds?: number[]): Promise<any> {
151 if (copyIds && copyIds.length > 0) {
152 // Reloading copies that were just edited.
153 return this.fetchCopies(copyIds);
155 } else if (this.context.session) {
156 this.context.sessionType = 'mixed';
157 return this.fetchSession(this.context.session);
159 } else if (this.context.copyId) {
160 this.context.sessionType = 'copy';
161 return this.fetchCopies(this.context.copyId);
163 } else if (this.context.volId) {
164 this.context.sessionType = 'vol';
165 return this.fetchVols(this.context.volId);
167 } else if (this.context.recordId) {
168 this.context.sessionType = 'record';
169 return this.fetchRecords(this.context.recordId);
173 // Changing a tab in the UI means changing the route.
174 // Changing the route ultimately results in changing the tab.
175 beforeTabChange(evt: NgbNavChangeEvent) {
176 evt.preventDefault();
177 this.tab = evt.nextId;
183 `/staff/cat/volcopy/${this.tab}/${this.target}/${this.targetId}`;
185 // Retain search parameters
186 this.router.navigate([url], {queryParamsHandling: 'merge'});
189 fetchSession(session: string): Promise<any> {
191 return this.cache.getItem(session, 'edit-these-copies')
192 .then((editSession: EditSession) => {
195 this.loading = false;
196 this.sessionExpired = true;
197 return Promise.reject('Session Expired');
200 console.debug('Edit Session', editSession);
202 this.context.recordId = editSession.record_id;
204 if (editSession.copies && editSession.copies.length > 0) {
205 return this.fetchCopies(editSession.copies);
208 const volsToFetch = [];
209 const volsToCreate = [];
210 editSession.raw.forEach((volData: CallNumData) => {
211 this.context.fastAdd = volData.fast_add === true;
213 if (volData.callnumber > 0) {
214 volsToFetch.push(volData);
216 volsToCreate.push(volData);
220 let promise = Promise.resolve();
221 if (volsToFetch.length > 0) {
222 promise = promise.then(_ =>
223 this.fetchVolsStubCopies(volsToFetch));
226 if (volsToCreate.length > 0) {
227 promise = promise.then(_ =>
228 this.createVolsStubCopies(volsToCreate));
235 // Creating new vols. Each gets a stub copy.
236 createVolsStubCopies(volDataList: CallNumData[]): Promise<any> {
239 volDataList.forEach(volData => {
241 const vol = this.volcopy.createStubVol(
242 this.context.recordId,
243 volData.owner || this.auth.user().ws_ou()
246 if (volData.label) {vol.label(volData.label); }
248 volData.callnumber = vol.id(); // wanted by addStubCopies
250 this.context.findOrCreateVolNode(vol);
253 return this.addStubCopies(vols, volDataList)
254 .then(_ => this.volcopy.setVolClassLabels(vols));
257 // Fetch vols by ID, but instead of retrieving their copies
258 // add a stub copy to each.
259 fetchVolsStubCopies(volDataList: CallNumData[]): Promise<any> {
261 const volIds = volDataList.map(volData => volData.callnumber);
264 return this.pcrud.search('acn', {id: volIds})
265 .pipe(tap((vol: IdlObject) => vols.push(vol))).toPromise()
266 .then(_ => this.addStubCopies(vols, volDataList));
269 // Add a stub copy to each vol using data from the edit session.
270 addStubCopies(vols: IdlObject[], volDataList: CallNumData[]): Promise<any> {
273 vols.forEach(vol => {
274 const volData = volDataList.filter(
275 vData => vData.callnumber === vol.id())[0];
278 this.volcopy.createStubCopy(vol, {circLib: volData.owner});
280 this.context.findOrCreateCopyNode(copy);
284 return this.volcopy.setCopyStatus(copies, this.context.fastAdd);
287 fetchCopies(copyIds: number | number[]): Promise<any> {
288 const ids = [].concat(copyIds);
289 if (ids.length === 0) { return Promise.resolve(); }
290 return this.pcrud.search('acp', {id: ids}, COPY_FLESH)
291 .pipe(tap(copy => this.context.findOrCreateCopyNode(copy)))
295 // Fetch call numbers and linked copies by call number ids.
296 fetchVols(volIds?: number | number[]): Promise<any> {
297 const ids = [].concat(volIds);
298 if (ids.length === 0) { return Promise.resolve(); }
300 return this.pcrud.search('acn', {id: ids})
301 .pipe(tap(vol => this.context.findOrCreateVolNode(vol)))
302 .toPromise().then(_ => {
303 return this.pcrud.search('acp',
304 {call_number: ids, deleted: 'f'}, COPY_FLESH
305 ).pipe(tap(copy => this.context.findOrCreateCopyNode(copy))
310 // Fetch call numbers and copies by record ids.
311 fetchRecords(recordIds: number | number[]): Promise<any> {
312 const ids = [].concat(recordIds);
314 return this.pcrud.search('acn',
315 {record: ids, deleted: 'f', label: {'!=' : '##URI##'}},
316 {}, {idlist: true, atomic: true}
317 ).toPromise().then(volIds => this.fetchVols(volIds));
321 save(close?: boolean): Promise<any> {
324 // Volume update API wants volumes fleshed with copies, instead
325 // of the other way around, which is what we have here.
326 const volumes: IdlObject[] = [];
328 this.context.volNodes().forEach(volNode => {
329 const newVol = this.idl.clone(volNode.target);
330 const copies: IdlObject[] = [];
332 volNode.children.forEach(copyNode => {
333 const copy = copyNode.target;
335 if (copy.isnew() && !copy.barcode()) {
336 // A new copy w/ no barcode is a stub copy sitting
337 // on an empty call number. Ignore it.
341 if (copy.ischanged() || copy.isnew() || copy.isdeleted()) {
342 const copyClone = this.idl.clone(copy);
343 // De-flesh call number
344 copyClone.call_number(copy.call_number().id());
345 copies.push(copyClone);
349 newVol.copies(copies);
351 if (newVol.ischanged() || newVol.isnew() || copies.length > 0) {
352 volumes.push(newVol);
356 this.context.volsToDelete.forEach(vol => {
357 const cloneVol = this.idl.clone(vol);
358 // No need to flesh copies -- they'll be force deleted.
360 volumes.push(cloneVol);
363 this.context.copiesToDelete.forEach(copy => {
364 const cloneCopy = this.idl.clone(copy);
365 const copyVol = cloneCopy.call_number();
366 cloneCopy.call_number(copyVol.id()); // de-flesh
368 let vol = volumes.filter(v => v.id() === copyVol.id())[0];
371 vol.copies().push(cloneCopy);
373 vol = this.idl.clone(copyVol);
374 vol.copies([cloneCopy]);
380 // De-flesh before posting
381 volumes.forEach(vol => {
382 vol.copies().forEach(copy => {
383 ['editor', 'creator', 'location'].forEach(field => {
384 if (typeof copy[field]() === 'object') {
385 copy[field](copy[field]().id());
391 let promise: Promise<number[]> = Promise.resolve([]);
393 if (volumes.length > 0) {
394 promise = this.saveApi(volumes, false, close);
397 return promise.then(copyIds => {
399 // In addition to the copies edited in this update call,
400 // reload any other copies that were previously loaded.
401 const ids: any = {}; // dedupe
402 this.context.copyList()
404 .filter(id => id > 0) // scrub the new copy IDs
406 .forEach(id => ids[id] = true);
408 copyIds = Object.keys(ids).map(id => Number(id));
411 return this.openPrintLabels(copyIds)
412 .then(_ => setTimeout(() => window.close()));
415 return this.load(Object.keys(ids).map(id => Number(id)));
418 this.loading = false;
419 this.changesPending = false;
423 broadcastChanges(volumes: IdlObject[]) {
425 const volIds = volumes.map(v => v.id());
429 volumes.forEach(vol => {
430 if (!recIds.includes(vol.record())) {
431 recIds.push(vol.record());
433 vol.copies().forEach(copy => copyIds.push(copy.id()));
436 this.broadcaster.broadcast('eg.holdings.update', {
443 saveApi(volumes: IdlObject[], override?:
444 boolean, close?: boolean): Promise<number[]> {
446 let method = 'open-ils.cat.asset.volume.fleshed.batch.update';
447 if (override) { method += '.override'; }
449 return this.net.request('open-ils.cat',
450 method, this.auth.token(), volumes, true,
451 { auto_merge_vols: true,
453 return_copy_ids: true,
454 force_delete_copies: true
457 ).toPromise().then(copyIds => {
459 const evt = this.evt.parse(copyIds);
462 // TODO: handle overrides?
463 // return this.saveApi(volumes, true, close);
464 this.loading = false;
466 return Promise.reject();
469 this.broadcastChanges(volumes);
475 toggleCheckbox(field: string) {
476 this.volcopy.defaults.values[field] =
477 !this.volcopy.defaults.values[field];
478 this.volcopy.saveDefaults();
481 openPrintLabels(copyIds?: number[]): Promise<any> {
482 if (!this.volcopy.defaults.values.print_labels) {
483 return Promise.resolve();
486 if (!copyIds || copyIds.length === 0) {
487 copyIds = this.context.copyList()
488 .map(c => c.id()).filter(id => id > 0);
491 return this.net.request(
493 'open-ils.actor.anon_cache.set_value',
494 null, 'print-labels-these-copies', {copies : copyIds}
496 ).toPromise().then(key => {
498 const url = '/eg/staff/cat/printlabels/' + key;
499 setTimeout(() => window.open(url, '_blank'));
503 isNotSaveable(): boolean {
504 return !(this.volsCanSave && this.attrsCanSave);
507 volsCanSaveChange(can: boolean) {
508 this.volsCanSave = can;
509 this.changesPending = true;
512 attrsCanSaveChange(can: boolean) {
513 this.attrsCanSave = can;
514 this.changesPending = true;
517 @HostListener('window:beforeunload', ['$event'])
518 canDeactivate($event?: Event): Promise<boolean> {
520 if (!this.changesPending) { return Promise.resolve(true); }
522 // Each warning dialog clears the current "changes are pending"
523 // flag so the user is not presented with the dialog again
524 // unless new changes are made.
525 this.changesPending = false;
527 if ($event) { // window.onbeforeunload
528 $event.preventDefault();
529 $event.returnValue = true;
531 } else { // tab OR route change.
532 return this.pendingChangesDialog.open().toPromise();