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';
18 import {CopyAttrsComponent} from './copy-attrs.component';
24 'call_number', 'location', 'parts', 'tags',
25 'creator', 'editor', 'stat_cat_entries', 'notes',
33 interface EditSession {
35 // Unset if editing in multi-record mode
41 // Adding to or creating new call numbers
44 // Hide the volumes editor
47 // Hide the copy attrs editor.
52 templateUrl: 'volcopy.component.html'
54 export class VolCopyComponent implements OnInit {
56 context: VolCopyContext;
58 sessionExpired = false;
60 tab = 'holdings'; // holdings | attrs | config
61 target: string; // item | callnumber | record | session
62 targetId: string; // id value or session string
66 changesPending = false;
67 routingAllowed = false;
69 @ViewChild('pendingChangesDialog', {static: false})
70 pendingChangesDialog: ConfirmDialogComponent;
72 @ViewChild('copyAttrs', {static: false}) copyAttrs: CopyAttrsComponent;
75 private router: Router,
76 private route: ActivatedRoute,
77 private evt: EventService,
78 private idl: IdlService,
79 private org: OrgService,
80 private net: NetService,
81 private auth: AuthService,
82 private pcrud: PcrudService,
83 private cache: AnonCacheService,
84 private broadcaster: BroadcastService,
85 private holdings: HoldingsService,
86 private volcopy: VolCopyService
90 this.route.paramMap.subscribe(
91 (params: ParamMap) => this.negotiateRoute(params));
94 negotiateRoute(params: ParamMap) {
95 this.tab = params.get('tab') || 'holdings';
96 this.target = params.get('target');
97 this.targetId = params.get('target_id');
99 if (this.volcopy.currentContext) {
100 // Avoid clobbering the context on route change.
101 this.context = this.volcopy.currentContext;
103 this.context = new VolCopyContext();
104 this.context.org = this.org; // inject;
107 switch (this.target) {
109 this.context.copyId = +this.targetId;
112 this.context.volId = +this.targetId;
115 this.context.recordId = +this.targetId;
118 this.context.session = this.targetId;
122 if (this.volcopy.currentContext) {
123 this.loading = false;
126 // Avoid refetching the data during route changes.
127 this.volcopy.currentContext = this.context;
132 load(copyIds?: number[]): Promise<any> {
133 this.sessionExpired = false;
135 this.context.reset();
137 return this.volcopy.load()
138 .then(_ => this.fetchHoldings(copyIds))
139 .then(_ => this.volcopy.applyVolLabels(
140 this.context.volNodes().map(n => n.target)))
141 .then(_ => this.context.sortHoldings())
142 .then(_ => this.context.setRecordId())
144 // unified display has no 'attrs' tab
145 if (this.volcopy.defaults.values.unified_display
146 && this.tab === 'attrs') {
147 this.tab = 'holdings';
151 .then(_ => this.loading = false);
154 fetchHoldings(copyIds?: number[]): Promise<any> {
156 if (copyIds && copyIds.length > 0) {
157 // Reloading copies that were just edited.
158 return this.fetchCopies(copyIds);
160 } else if (this.context.session) {
161 this.context.sessionType = 'mixed';
162 return this.fetchSession(this.context.session);
164 } else if (this.context.copyId) {
165 this.context.sessionType = 'copy';
166 return this.fetchCopies(this.context.copyId);
168 } else if (this.context.volId) {
169 this.context.sessionType = 'vol';
170 return this.fetchVols(this.context.volId);
172 } else if (this.context.recordId) {
173 this.context.sessionType = 'record';
174 return this.fetchRecords(this.context.recordId);
178 // Changing a tab in the UI means changing the route.
179 // Changing the route ultimately results in changing the tab.
180 beforeTabChange(evt: NgbNavChangeEvent) {
181 evt.preventDefault();
183 // Always allow routing between tabs since no changes are lost
184 // in the process. In some cases, this is necessary to avoid
185 // "pending changes" alerts while you are trying to resolve
186 // other issues (e.g. applying values for required fields).
187 this.routingAllowed = true;
188 this.tab = evt.nextId;
194 `/staff/cat/volcopy/${this.tab}/${this.target}/${this.targetId}`;
196 // Retain search parameters
197 this.router.navigate([url], {queryParamsHandling: 'merge'});
200 fetchSession(session: string): Promise<any> {
202 return this.cache.getItem(session, 'edit-these-copies')
203 .then((editSession: EditSession) => {
206 this.loading = false;
207 this.sessionExpired = true;
208 return Promise.reject('Session Expired');
211 console.debug('Edit Session', editSession);
213 this.context.recordId = editSession.record_id;
215 if (editSession.copies && editSession.copies.length > 0) {
216 return this.fetchCopies(editSession.copies);
219 const volsToFetch = [];
220 const volsToCreate = [];
221 editSession.raw.forEach((volData: CallNumData) => {
222 this.context.fastAdd = volData.fast_add === true;
224 if (volData.callnumber > 0) {
225 volsToFetch.push(volData);
227 volsToCreate.push(volData);
231 let promise = Promise.resolve();
232 if (volsToFetch.length > 0) {
233 promise = promise.then(_ =>
234 this.fetchVolsStubCopies(volsToFetch));
237 if (volsToCreate.length > 0) {
238 promise = promise.then(_ =>
239 this.createVolsStubCopies(volsToCreate));
246 // Creating new vols. Each gets a stub copy.
247 createVolsStubCopies(volDataList: CallNumData[]): Promise<any> {
250 volDataList.forEach(volData => {
252 const vol = this.volcopy.createStubVol(
253 this.context.recordId,
254 volData.owner || this.auth.user().ws_ou()
257 if (volData.label) {vol.label(volData.label); }
259 volData.callnumber = vol.id(); // wanted by addStubCopies
261 this.context.findOrCreateVolNode(vol);
264 return this.addStubCopies(vols, volDataList)
265 .then(_ => this.volcopy.setVolClassLabels(vols));
268 // Fetch vols by ID, but instead of retrieving their copies
269 // add a stub copy to each.
270 fetchVolsStubCopies(volDataList: CallNumData[]): Promise<any> {
272 const volIds = volDataList.map(volData => volData.callnumber);
275 return this.pcrud.search('acn', {id: volIds})
276 .pipe(tap((vol: IdlObject) => vols.push(vol))).toPromise()
277 .then(_ => this.addStubCopies(vols, volDataList));
280 // Add a stub copy to each vol using data from the edit session.
281 addStubCopies(vols: IdlObject[], volDataList: CallNumData[]): Promise<any> {
284 vols.forEach(vol => {
285 const volData = volDataList.filter(
286 vData => vData.callnumber === vol.id())[0];
289 this.volcopy.createStubCopy(vol, {circLib: volData.owner});
291 this.context.findOrCreateCopyNode(copy);
295 return this.volcopy.setCopyStatus(copies);
298 fetchCopies(copyIds: number | number[]): Promise<any> {
299 const ids = [].concat(copyIds);
300 if (ids.length === 0) { return Promise.resolve(); }
301 return this.pcrud.search('acp', {id: ids}, COPY_FLESH)
302 .pipe(tap(copy => this.context.findOrCreateCopyNode(copy)))
306 // Fetch call numbers and linked copies by call number ids.
307 fetchVols(volIds?: number | number[]): Promise<any> {
308 const ids = [].concat(volIds);
309 if (ids.length === 0) { return Promise.resolve(); }
311 return this.pcrud.search('acn', {id: ids})
312 .pipe(tap(vol => this.context.findOrCreateVolNode(vol)))
313 .toPromise().then(_ => {
314 return this.pcrud.search('acp',
315 {call_number: ids, deleted: 'f'}, COPY_FLESH
316 ).pipe(tap(copy => this.context.findOrCreateCopyNode(copy))
321 // Fetch call numbers and copies by record ids.
322 fetchRecords(recordIds: number | number[]): Promise<any> {
323 const ids = [].concat(recordIds);
325 return this.pcrud.search('acn',
326 {record: ids, deleted: 'f', label: {'!=' : '##URI##'}},
327 {}, {idlist: true, atomic: true}
328 ).toPromise().then(volIds => this.fetchVols(volIds));
332 save(close?: boolean): Promise<any> {
335 if (this.copyAttrs) {
336 // Won't exist on any non-attrs page.
337 this.copyAttrs.applyPendingChanges();
340 // Volume update API wants volumes fleshed with copies, instead
341 // of the other way around, which is what we have here.
342 const volumes: IdlObject[] = [];
344 this.context.volNodes().forEach(volNode => {
345 const newVol = this.idl.clone(volNode.target);
346 const copies: IdlObject[] = [];
348 volNode.children.forEach(copyNode => {
349 const copy = copyNode.target;
351 if (copy.isnew() && !copy.barcode()) {
352 // A new copy w/ no barcode is a stub copy sitting
353 // on an empty call number. Ignore it.
357 // Be sure to include copies when the volume is changed
358 // without any changes to the copies. This ensures the
359 // API knows when we are modifying a subset of the total
360 // copies on a volume, e.g. when changing volume labels
361 if (newVol.ischanged()) { copy.ischanged(true); }
363 if (copy.ischanged() || copy.isnew() || copy.isdeleted()) {
364 const copyClone = this.idl.clone(copy);
365 // De-flesh call number
366 copyClone.call_number(copy.call_number().id());
367 copies.push(copyClone);
371 newVol.copies(copies);
373 if (newVol.ischanged() || newVol.isnew() || copies.length > 0) {
374 volumes.push(newVol);
378 this.context.volsToDelete.forEach(vol => {
379 const cloneVol = this.idl.clone(vol);
380 // No need to flesh copies -- they'll be force deleted.
382 volumes.push(cloneVol);
385 this.context.copiesToDelete.forEach(copy => {
386 const cloneCopy = this.idl.clone(copy);
387 const copyVol = cloneCopy.call_number();
388 cloneCopy.call_number(copyVol.id()); // de-flesh
390 let vol = volumes.filter(v => v.id() === copyVol.id())[0];
393 vol.copies().push(cloneCopy);
395 vol = this.idl.clone(copyVol);
396 vol.copies([cloneCopy]);
402 // De-flesh before posting
403 volumes.forEach(vol => {
404 vol.copies().forEach(copy => {
405 ['editor', 'creator', 'location'].forEach(field => {
406 if (typeof copy[field]() === 'object') {
407 copy[field](copy[field]().id());
413 let promise: Promise<number[]> = Promise.resolve([]);
415 if (volumes.length > 0) {
416 promise = this.saveApi(volumes, false, close);
419 return promise.then(copyIds => {
421 // In addition to the copies edited in this update call,
422 // reload any other copies that were previously loaded.
423 const ids: any = {}; // dedupe
424 this.context.copyList()
426 .filter(id => id > 0) // scrub the new copy IDs
428 .forEach(id => ids[id] = true);
430 copyIds = Object.keys(ids).map(id => Number(id));
433 return this.openPrintLabels(copyIds)
434 .then(_ => setTimeout(() => window.close()));
437 return this.load(Object.keys(ids).map(id => Number(id)));
440 this.loading = false;
441 this.changesPending = false;
445 broadcastChanges(volumes: IdlObject[]) {
447 const volIds = volumes.map(v => v.id());
451 volumes.forEach(vol => {
452 if (!recIds.includes(vol.record())) {
453 recIds.push(vol.record());
455 vol.copies().forEach(copy => copyIds.push(copy.id()));
458 this.broadcaster.broadcast('eg.holdings.update', {
465 saveApi(volumes: IdlObject[], override?:
466 boolean, close?: boolean): Promise<number[]> {
468 let method = 'open-ils.cat.asset.volume.fleshed.batch.update';
469 if (override) { method += '.override'; }
471 return this.net.request('open-ils.cat',
472 method, this.auth.token(), volumes, true,
473 { auto_merge_vols: true,
475 return_copy_ids: true,
476 force_delete_copies: true
479 ).toPromise().then(copyIds => {
481 const evt = this.evt.parse(copyIds);
484 // TODO: handle overrides?
485 // return this.saveApi(volumes, true, close);
486 this.loading = false;
488 return Promise.reject();
491 this.broadcastChanges(volumes);
497 toggleCheckbox(field: string) {
498 this.volcopy.defaults.values[field] =
499 !this.volcopy.defaults.values[field];
500 this.volcopy.saveDefaults();
503 openPrintLabels(copyIds?: number[]): Promise<any> {
504 if (!this.volcopy.defaults.values.print_labels) {
505 return Promise.resolve();
508 if (!copyIds || copyIds.length === 0) {
509 copyIds = this.context.copyList()
510 .map(c => c.id()).filter(id => id > 0);
513 return this.net.request(
515 'open-ils.actor.anon_cache.set_value',
516 null, 'print-labels-these-copies', {copies : copyIds}
518 ).toPromise().then(key => {
520 const url = '/eg/staff/cat/printlabels/' + key;
521 setTimeout(() => window.open(url, '_blank'));
525 isNotSaveable(): boolean {
527 if (!this.volsCanSave) { return true; }
528 if (!this.attrsCanSave) { return true; }
530 // This can happen regardless of whether we are modifying
531 // volumes vs. copies.
532 if (this.volcopy.missingRequiredStatCat()) { return true; }
537 volsCanSaveChange(can: boolean) {
538 this.volsCanSave = can;
539 this.changesPending = true;
542 attrsCanSaveChange(can: boolean) {
543 this.attrsCanSave = can;
544 this.changesPending = true;
547 @HostListener('window:beforeunload', ['$event'])
548 canDeactivate($event?: Event): boolean | Promise<boolean> {
550 if (this.routingAllowed) {
551 // We call canDeactive manually when routing between volcopy
552 // tabs. If routingAllowed, it means we'ave already confirmed
553 // the tag change is OK.
554 this.routingAllowed = false;
558 const editing = this.copyAttrs ? this.copyAttrs.hasActiveInput() : false;
560 if (!editing && !this.changesPending) { return true; }
562 // Each warning dialog clears the current "changes are pending"
563 // flag so the user is not presented with the dialog again
564 // unless new changes are made. The 'editing' value will reset
565 // since the attrs component is getting destroyed.
566 this.changesPending = false;
568 if ($event) { // window.onbeforeunload
569 $event.preventDefault();
570 $event.returnValue = true;
572 } else { // tab OR route change.
573 return this.pendingChangesDialog.open().toPromise();