1 import {Component, OnInit, AfterViewInit, ViewChild, Input, Renderer2, Output, EventEmitter} from '@angular/core';
2 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
3 import {tap} from 'rxjs/operators';
4 import {IdlService, IdlObject} from '@eg/core/idl.service';
5 import {OrgService} from '@eg/core/org.service';
6 import {AuthService} from '@eg/core/auth.service';
7 import {NetService} from '@eg/core/net.service';
8 import {PcrudService} from '@eg/core/pcrud.service';
9 import {VolCopyContext, HoldingsTreeNode} from './volcopy';
10 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
11 import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
12 import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
13 import {VolCopyService} from './volcopy.service';
16 selector: 'eg-vol-edit',
17 templateUrl: 'vol-edit.component.html',
18 styleUrls: ['vol-edit.component.css']
22 export class VolEditComponent implements OnInit {
24 @Input() context: VolCopyContext;
26 // There are 10 columns in the editor form. Set the flex values
27 // here so they don't have to be hard-coded and repeated in the
28 // markup. Changing a flex value here will propagate to all
29 // rows in the form. Column numbers are 1-based.
30 flexSettings: {[column: number]: number} = {
31 1: 1, 2: 1, 3: 2, 4: 1, 5: 2, 6: 1, 7: 1, 8: 2, 9: 1, 10: 1};
33 // If a column is specified as the expand field, its flex value
34 // will magically grow.
37 batchVolClass: ComboboxEntry;
38 batchVolPrefix: ComboboxEntry;
39 batchVolSuffix: ComboboxEntry;
40 batchVolLabel: ComboboxEntry;
42 autoBarcodeInProgress = false;
43 useCheckdigit = false;
45 deleteVolCount: number = null;
46 deleteCopyCount: number = null;
48 recordVolLabels: string[] = [];
50 @ViewChild('confirmDelVol', {static: false})
51 confirmDelVol: ConfirmDialogComponent;
53 @ViewChild('confirmDelCopy', {static: false})
54 confirmDelCopy: ConfirmDialogComponent;
56 // Emitted when the save-ability of this form changes.
57 @Output() canSaveChange: EventEmitter<boolean> = new EventEmitter<boolean>();
60 private renderer: Renderer2,
61 private idl: IdlService,
62 private org: OrgService,
63 private pcrud: PcrudService,
64 private net: NetService,
65 private auth: AuthService,
66 private holdings: HoldingsService,
67 public volcopy: VolCopyService
72 this.deleteVolCount = null;
73 this.deleteCopyCount = null;
74 this.useCheckdigit = this.volcopy.defaults.values.use_checkdigit;
76 this.volcopy.fetchRecordVolLabels(this.context.recordId)
77 .then(labels => this.recordVolLabels = labels)
78 .then(_ => this.volcopy.fetchBibParts(this.context.getRecordIds()))
79 .then(_ => this.addStubCopies());
82 copyStatLabel(copy: IdlObject): string {
84 const statId = copy.status();
85 if (statId in this.volcopy.copyStatuses) {
86 return this.volcopy.copyStatuses[statId].name();
92 recordHasParts(bibId: number): boolean {
93 return this.volcopy.bibParts[bibId] &&
94 this.volcopy.bibParts[bibId].length > 0;
97 // Column width (flex:x) for column by column number.
98 flexAt(column: number): number {
99 return this.flexSpan(column, column);
102 // Returns the flex amount occupied by a span of columns.
103 flexSpan(column1: number, column2: number): number {
105 for (let i = column1; i <= column2; i++) {
106 let value = this.flexSettings[i];
107 if (this.expand === i) { value = value * 3; }
113 volCountChanged(orgNode: HoldingsTreeNode, count: number) {
114 if (count === null) { return; }
115 const diff = count - orgNode.children.length;
117 this.createVols(orgNode, diff);
118 } else if (diff < 0) {
119 this.deleteVols(orgNode, -diff);
124 addVol(org: IdlObject) {
125 if (!org) { return; }
126 const orgNode = this.context.findOrCreateOrgNode(org.id());
127 this.createVols(orgNode, 1);
130 existingVolCount(orgNode: HoldingsTreeNode): number {
131 return orgNode.children.filter(volNode => !volNode.target.isnew()).length;
134 existingCopyCount(volNode: HoldingsTreeNode): number {
135 return volNode.children.filter(copyNode => !copyNode.target.isnew()).length;
138 copyCountChanged(volNode: HoldingsTreeNode, count: number) {
139 if (count === null) { return; }
140 const diff = count - volNode.children.length;
142 this.createCopies(volNode, diff);
143 } else if (diff < 0) {
144 this.deleteCopies(volNode, -diff);
148 // This only removes copies that were created during the
149 // current editing session and have not yet been saved in the DB.
150 deleteCopies(volNode: HoldingsTreeNode, count: number) {
151 for (let i = 0; i < count; i++) {
152 const copyNode = volNode.children[volNode.children.length - 1];
153 if (copyNode && copyNode.target.isnew()) {
154 volNode.children.pop();
161 createCopies(volNode: HoldingsTreeNode, count: number) {
162 for (let i = 0; i < count; i++) {
164 // Our context assumes copies are fleshed with volumes
165 const vol = volNode.target;
166 const copy = this.volcopy.createStubCopy(vol);
167 copy.call_number(vol);
168 this.context.findOrCreateCopyNode(copy);
173 createVols(orgNode: HoldingsTreeNode, count: number) {
175 for (let i = 0; i < count; i++) {
177 // This will vivify the volNode if needed.
178 const vol = this.volcopy.createStubVol(
179 this.context.recordId, orgNode.target.id());
183 // Our context assumes copies are fleshed with volumes
184 const copy = this.volcopy.createStubCopy(vol);
185 copy.call_number(vol);
186 this.context.findOrCreateCopyNode(copy);
189 this.volcopy.setVolClassLabels(vols);
192 // This only removes vols that were created during the
193 // current editing session and have not yet been saved in the DB.
194 deleteVols(orgNode: HoldingsTreeNode, count: number) {
195 for (let i = 0; i < count; i++) {
196 const volNode = orgNode.children[orgNode.children.length - 1];
197 if (volNode && volNode.target.isnew()) {
198 orgNode.children.pop();
205 // When editing existing vols, be sure each has at least one copy.
206 addStubCopies(volNode?: HoldingsTreeNode) {
207 const nodes = volNode ? [volNode] : this.context.volNodes();
209 nodes.forEach(vNode => {
210 if (vNode.children.length === 0) {
211 const vol = vNode.target;
212 const copy = this.volcopy.createStubCopy(vol);
213 copy.call_number(vol);
214 this.context.findOrCreateCopyNode(copy);
219 applyVolValue(vol: IdlObject, key: string, value: any) {
221 if (value === null && (key === 'prefix' || key === 'suffix')) {
222 // -1 is the empty prefix/suffix value.
226 if (vol[key]() !== value) {
231 this.emitSaveChange();
234 applyCopyValue(copy: IdlObject, key: string, value: any) {
235 if (copy[key]() !== value) {
237 copy.ischanged(true);
241 copyPartChanged(copyNode: HoldingsTreeNode, entry: ComboboxEntry) {
242 const copy = copyNode.target;
243 const part = copyNode.target.parts()[0];
248 this.volcopy.bibParts[copy.call_number().record()]
249 .filter(p => p.id() === entry.id)[0];
251 // Nothing to change?
252 if (part && part.id() === newPart.id()) { return; }
254 copy.parts([newPart]);
255 copy.ischanged(true);
257 } else if (part) { // Part map no longer needed.
260 copy.ischanged(true);
265 this.context.volNodes().forEach(volNode => {
266 const vol = volNode.target;
267 if (this.batchVolClass) {
268 this.applyVolValue(vol, 'label_class', this.batchVolClass.id);
270 if (this.batchVolPrefix) {
271 this.applyVolValue(vol, 'prefix', this.batchVolPrefix.id);
273 if (this.batchVolSuffix) {
274 this.applyVolValue(vol, 'suffix', this.batchVolSuffix.id);
276 if (this.batchVolLabel) {
277 // Use label; could be freetext.
278 this.applyVolValue(vol, 'label', this.batchVolLabel.label);
283 // Focus and select the next editable barcode.
284 selectNextBarcode(id: number, previous?: boolean) {
286 let nextId: number = null;
287 let firstId: number = null;
289 let copies = this.context.copyList();
290 if (previous) { copies = copies.reverse(); }
292 // Find the ID of the next item. If this is the last item,
293 // loop back to the first item.
294 copies.forEach(copy => {
295 if (nextId !== null) { return; }
297 // In case we have to loop back to the first copy.
298 if (firstId === null && this.barcodeCanChange(copy)) {
303 if (nextId === null && this.barcodeCanChange(copy)) {
306 } else if (copy.id() === id) {
311 this.renderer.selectRootElement(
312 '#barcode-input-' + (nextId || firstId)).select();
315 barcodeCanChange(copy: IdlObject): boolean {
316 return !this.volcopy.copyStatIsMagic(copy.status());
320 this.autoBarcodeInProgress = true;
322 // Autogen only replaces barcodes for items which are in
324 const copies = this.context.copyList()
325 .filter((copy, idx) => {
326 // During autogen we do not replace the first item,
327 // so it's status is not relevant.
328 return idx === 0 || this.barcodeCanChange(copy);
331 if (copies.length > 1) { // seed barcode will always be present
332 this.proceedWithAutogen(copies)
333 .then(_ => this.autoBarcodeInProgress = false);
337 proceedWithAutogen(copyList: IdlObject[]): Promise<any> {
339 const seedBarcode: string = copyList[0].barcode();
340 copyList.shift(); // Avoid replacing the seed barcode
342 const count = copyList.length;
344 return this.net.request('open-ils.cat',
345 'open-ils.cat.item.barcode.autogen',
346 this.auth.token(), seedBarcode, count, {
347 checkdigit: this.useCheckdigit,
350 ).pipe(tap(barcodes => {
352 copyList.forEach(copy => {
353 if (copy.barcode() !== barcodes[0]) {
354 copy.barcode(barcodes[0]);
355 copy.ischanged(true);
363 barcodeChanged(copy: IdlObject, barcode: string) {
364 // note: copy.barcode(barcode) applied via ngModel
365 copy.ischanged(true);
366 copy._dupe_barcode = false;
369 this.emitSaveChange();
373 if (!this.autoBarcodeInProgress) {
374 // Manual barcode entry requires dupe check
376 copy._dupe_barcode = false;
377 this.pcrud.search('acp', {
380 id: {'!=': copy.id()}
383 if (resp) { copy._dupe_barcode = true; }
386 () => this.emitSaveChange()
391 deleteCopy(copyNode: HoldingsTreeNode) {
393 if (copyNode.target.isnew()) {
394 // Confirmation not required when deleting brand new copies.
395 this.deleteOneCopy(copyNode);
399 this.deleteCopyCount = 1;
400 this.confirmDelCopy.open().toPromise().then(confirmed => {
401 if (confirmed) { this.deleteOneCopy(copyNode); }
405 deleteOneCopy(copyNode: HoldingsTreeNode) {
406 const targetCopy = copyNode.target;
408 const orgNodes = this.context.orgNodes();
409 for (let orgIdx = 0; orgIdx < orgNodes.length; orgIdx++) {
410 const orgNode = orgNodes[orgIdx];
412 for (let volIdx = 0; volIdx < orgNode.children.length; volIdx++) {
413 const volNode = orgNode.children[volIdx];
415 for (let copyIdx = 0; copyIdx < volNode.children.length; copyIdx++) {
416 const copy = volNode.children[copyIdx].target;
418 if (copy.id() === targetCopy.id()) {
419 volNode.children.splice(copyIdx, 1);
421 copy.isdeleted(true);
422 this.context.copiesToDelete.push(copy);
425 if (volNode.children.length === 0) {
426 // When removing the last copy, add a stub copy.
427 this.addStubCopies();
438 deleteVol(volNode: HoldingsTreeNode) {
440 if (volNode.target.isnew()) {
441 // Confirmation not required when deleting brand new vols.
442 this.deleteOneVol(volNode);
446 this.deleteVolCount = 1;
447 this.deleteCopyCount = volNode.children.length;
449 this.confirmDelVol.open().toPromise().then(confirmed => {
450 if (confirmed) { this.deleteOneVol(volNode); }
454 deleteOneVol(volNode: HoldingsTreeNode) {
456 let deleteVolIdx = null;
457 const targetVol = volNode.target;
459 // FOR loops allow for early exit
460 const orgNodes = this.context.orgNodes();
461 for (let orgIdx = 0; orgIdx < orgNodes.length; orgIdx++) {
462 const orgNode = orgNodes[orgIdx];
464 for (let volIdx = 0; volIdx < orgNode.children.length; volIdx++) {
465 const vol = orgNode.children[volIdx].target;
467 if (vol.id() === targetVol.id()) {
468 deleteVolIdx = volIdx;
471 // New volumes, which can only have new copies
472 // may simply be removed from the holdings
473 // tree to delete them.
477 // Mark volume and attached copies as deleted
478 // and track for later deletion.
479 targetVol.isdeleted(true);
480 this.context.volsToDelete.push(targetVol);
482 // When deleting vols, no need to delete the linked
483 // copies. They'll be force deleted via the API.
486 if (deleteVolIdx !== null) { break; }
489 if (deleteVolIdx !== null) {
490 orgNode.children.splice(deleteVolIdx, 1);
496 displayColumn(field: string): boolean {
497 return this.volcopy.defaults.hidden[field] !== true;
500 saveUseCheckdigit() {
501 this.volcopy.defaults.values.use_checkdigit = this.useCheckdigit === true;
502 this.volcopy.saveDefaults();
507 const copies = this.context.copyList();
509 const badCopies = copies.filter(copy => {
510 return copy._dupe_barcode || (!copy.isnew() && !copy.barcode());
513 if (badCopies) { return false; }
515 const badVols = this.context.volNodes().filter(volNode => {
516 const vol = volNode.target;
518 vol.prefix() && vol.label() && vol.suffix && vol.label_class()
525 // Called any time a change occurs that could affect the
526 // save-ability of the form.
529 this.canSaveChange.emit(this.canSave());