]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/acq/lineitem/lineitem-list.component.ts
LP1929741 ACQ Selection List & PO Angluar Port
[Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / acq / lineitem / lineitem-list.component.ts
1 import {Component, OnInit, Input, Output, ViewChild} from '@angular/core';
2 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
3 import {Observable} from 'rxjs';
4 import {tap} from 'rxjs/operators';
5 import {Pager} from '@eg/share/util/pager';
6 import {EgEvent, EventService} from '@eg/core/event.service';
7 import {IdlObject} from '@eg/core/idl.service';
8 import {NetService} from '@eg/core/net.service';
9 import {AuthService} from '@eg/core/auth.service';
10 import {ServerStoreService} from '@eg/core/server-store.service';
11 import {LineitemService} from './lineitem.service';
12 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
13 import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
14 import {CancelDialogComponent} from './cancel-dialog.component';
15
16 @Component({
17   templateUrl: 'lineitem-list.component.html',
18   selector: 'eg-lineitem-list',
19   styleUrls: ['lineitem-list.component.css']
20 })
21 export class LineitemListComponent implements OnInit {
22
23     picklistId: number = null;
24     poId: number = null;
25
26     loading = false;
27     pager: Pager = new Pager();
28     pageOfLineitems: IdlObject[] = [];
29     lineitemIds: number[] = [];
30
31     // Selected lineitems
32     selected: {[id: number]: boolean} = {};
33
34     // Order identifier type per lineitem
35     orderIdentTypes: {[id: number]: 'isbn' | 'issn' | 'upc'} = {};
36
37     // Copy counts per lineitem
38     existingCopyCounts: {[id: number]: number} = {};
39
40     // Squash these down to an easily traversable data set to avoid
41     // a lot of repetitive looping.
42     liMarcAttrs: {[id: number]: {[name: string]: IdlObject[]}} = {};
43
44     batchNote: string;
45     noteIsPublic = false;
46     batchSelectPage = false;
47     batchSelectAll = false;
48     showNotesFor: number;
49     showExpandFor: number; // 'Expand'
50     expandAll = false;
51     action = '';
52     batchFailure: EgEvent;
53     focusLi: number;
54
55     @ViewChild('cancelDialog') cancelDialog: CancelDialogComponent;
56
57     constructor(
58         private router: Router,
59         private route: ActivatedRoute,
60         private evt: EventService,
61         private net: NetService,
62         private auth: AuthService,
63         private store: ServerStoreService,
64         private holdings: HoldingsService,
65         private liService: LineitemService
66     ) {}
67
68     ngOnInit() {
69
70         this.route.queryParamMap.subscribe((params: ParamMap) => {
71             this.pager.offset = +params.get('offset');
72             this.pager.limit = +params.get('limit');
73             this.load();
74         });
75
76         this.route.fragment.subscribe((fragment: string) => {
77             const id = Number(fragment);
78             if (id > 0) { this.focusLineitem(id); }
79         });
80
81         this.route.parent.paramMap.subscribe((params: ParamMap) => {
82             this.picklistId = +params.get('picklistId');
83             this.poId = +params.get('poId');
84             this.load();
85         });
86
87         this.store.getItem('acq.lineitem.page_size').then(count => {
88             this.pager.setLimit(count || 20);
89             this.load();
90         });
91     }
92
93     pageSizeChange(count: number) {
94         this.store.setItem('acq.lineitem.page_size', count).then(_ => {
95             this.pager.setLimit(count);
96             this.pager.toFirst();
97             this.goToPage();
98         });
99     }
100
101     // Focus the selected lineitem, which may not yet exist in the
102     // DOM for focusing.
103     focusLineitem(id?: number) {
104         if (id !== undefined) { this.focusLi = id; }
105         if (this.focusLi) {
106             const node = document.getElementById('' + this.focusLi);
107             if (node) { node.scrollIntoView(true); }
108         }
109     }
110
111     load(): Promise<any> {
112         this.pageOfLineitems = [];
113
114         if (!this.loading &&
115             this.pager.limit && (this.poId || this.picklistId)) {
116
117             this.loading = true;
118
119             return this.loadIds()
120                 .then(_ => this.loadPage())
121                 .then(_ => this.loading = false)
122                 .catch(_ => {}); // re-route while page is loading
123         }
124
125         // We have not collected enough data to proceed.
126         return Promise.resolve();
127
128     }
129
130     loadIds(): Promise<any> {
131         this.lineitemIds = [];
132
133         let id = this.poId;
134         let options: any = {flesh_lineitem_ids: true, li_limit: 10000};
135         let method = 'open-ils.acq.purchase_order.retrieve';
136         let handler = (po) => po.lineitems();
137
138         if (this.picklistId) {
139             id = this.picklistId;
140             options = {idlist: true, limit: 1000};
141             method = 'open-ils.acq.lineitem.picklist.retrieve.atomic';
142             handler = (ids) => ids;
143         }
144
145         return this.net.request(
146             'open-ils.acq', method, this.auth.token(), id, options
147         ).toPromise().then(resp => {
148             const ids = handler(resp);
149
150             this.lineitemIds = ids
151                 .map(i => Number(i))
152                 .sort((id1, id2) => id1 < id2 ? -1 : 1);
153
154             this.pager.resultCount = ids.length;
155         });
156     }
157
158     goToPage() {
159         this.focusLi = null;
160         this.router.navigate([], {
161             relativeTo: this.route,
162             queryParamsHandling: 'merge',
163             fragment: null,
164             queryParams: {
165                 offset: this.pager.offset,
166                 limit: this.pager.limit
167             }
168         });
169     }
170
171     loadPage(): Promise<any> {
172         return this.jumpToLiPage()
173             .then(_ => this.loadPageOfLis())
174             .then(_ => this.setBatchSelect())
175             .then(_ => setTimeout(() => this.focusLineitem()));
176     }
177
178     jumpToLiPage(): Promise<boolean> {
179         if (!this.focusLi) { return Promise.resolve(true); }
180
181         const idx = this.lineitemIds.indexOf(this.focusLi);
182         if (idx === -1) { return Promise.resolve(true); }
183
184         const offset = Math.floor(idx / this.pager.limit) * this.pager.limit;
185
186         return this.router.navigate(['./'], {
187             relativeTo: this.route,
188             queryParams: {offset: offset, limit: this.pager.limit},
189             fragment: '' + this.focusLi
190         });
191     }
192
193     loadPageOfLis(): Promise<any> {
194         this.pageOfLineitems = [];
195
196         const ids = this.lineitemIds.slice(
197             this.pager.offset, this.pager.offset + this.pager.limit)
198             .filter(id => id !== undefined);
199
200         if (ids.length === 0) { return Promise.resolve(); }
201
202         if (this.pageOfLineitems.length === ids.length) {
203             // All entries found in the cache
204             return Promise.resolve();
205         }
206
207         this.pageOfLineitems = []; // reset
208
209         return this.liService.getFleshedLineitems(
210             ids, {fromCache: true, toCache: true})
211         .pipe(tap(struct => {
212             this.ingestOneLi(struct.lineitem);
213             this.existingCopyCounts[struct.id] = struct.existing_copies;
214         })).toPromise();
215     }
216
217     ingestOneLi(li: IdlObject, replace?: boolean) {
218         this.liMarcAttrs[li.id()] = {};
219
220         li.attributes().forEach(attr => {
221             const name = attr.attr_name();
222             this.liMarcAttrs[li.id()][name] =
223                 this.liService.getAttributes(
224                     li, name, 'lineitem_marc_attr_definition');
225         });
226
227         const ident = this.liService.getOrderIdent(li);
228         this.orderIdentTypes[li.id()] = ident ? ident.attr_name() : 'isbn';
229
230         // newest to oldest
231         li.lineitem_notes(li.lineitem_notes().sort(
232             (n1, n2) => n1.create_time() < n2.create_time() ? 1 : -1));
233
234         if (replace) {
235             for (let idx = 0; idx < this.pageOfLineitems.length; idx++) {
236                 if (this.pageOfLineitems[idx].id() === li.id()) {
237                     this.pageOfLineitems[idx] = li;
238                     break;
239                 }
240             }
241         } else {
242             this.pageOfLineitems.push(li);
243         }
244     }
245
246     // First matching attr
247     displayAttr(li: IdlObject, name: string): string {
248         return (
249             this.liMarcAttrs[li.id()][name] &&
250             this.liMarcAttrs[li.id()][name][0]
251         ) ? this.liMarcAttrs[li.id()][name][0].attr_value() : '';
252     }
253
254     // All matching attrs
255     attrs(li: IdlObject, name: string, attrType?: string): IdlObject[] {
256         return this.liService.getAttributes(li, name, attrType);
257     }
258
259     jacketIdent(li: IdlObject): string {
260         return this.displayAttr(li, 'isbn') || this.displayAttr(li, 'upc');
261     }
262
263     // Order ident options are pulled from the MARC, but the ident
264     // value proper is stored as a local attr def.
265     identOptions(li: IdlObject): ComboboxEntry[] {
266         const otype = this.orderIdentTypes[li.id()];
267
268         if (this.liMarcAttrs[li.id()][otype]) {
269             return this.liMarcAttrs[li.id()][otype].map(
270                 attr => ({id: attr.id(), label: attr.attr_value()}));
271         }
272
273         return [];
274     }
275
276     // Returns the MARC attr with the same type and value as the applied
277     // order identifier (which is a local attr)
278     selectedIdent(li: IdlObject): number {
279         const ident = this.liService.getOrderIdent(li);
280         if (!ident) { return null; }
281
282         const attr = this.identOptions(li).filter(
283             (entry: ComboboxEntry) => entry.label === ident.attr_value())[0];
284         return attr ? attr.id : null;
285     }
286
287     currentIdent(li: IdlObject): IdlObject {
288         return this.liService.getOrderIdent(li);
289     }
290
291     orderIdentChanged(li: IdlObject, entry: ComboboxEntry) {
292         if (entry === null) { return; }
293
294         this.liService.changeOrderIdent(
295             li, entry.id, this.orderIdentTypes[li.id()], entry.label
296         ).subscribe(freshLi => this.ingestOneLi(freshLi, true));
297     }
298
299     addBriefRecord() {
300     }
301
302     selectedIds(): number[] {
303         return Object.keys(this.selected)
304             .filter(id => this.selected[id] === true)
305             .map(id => Number(id));
306     }
307
308
309     // After a page of LI's are loaded, see if the batch-select checkbox
310     // needs to be on or off.
311     setBatchSelect() {
312         let on = true;
313         const ids = this.selectedIds();
314         this.pageOfLineitems.forEach(li => {
315             if (!ids.includes(li.id())) { on = false; }
316         });
317
318         this.batchSelectPage = on;
319
320         on = true;
321
322         this.lineitemIds.forEach(id => {
323             if (!this.selected[id]) { on = false; }
324         });
325
326         this.batchSelectAll = on;
327     }
328
329     toggleSelectAll(allItems: boolean) {
330
331         if (allItems) {
332             this.lineitemIds.forEach(
333                 id => this.selected[id] = this.batchSelectAll);
334
335             this.batchSelectPage = this.batchSelectAll;
336
337         } else {
338
339             this.pageOfLineitems.forEach(
340                 li => this.selected[li.id()] = this.batchSelectPage);
341
342             if (!this.batchSelectPage) {
343                 // When deselecting items in the page, we're no longer
344                 // selecting all items.
345                 this.batchSelectAll = false;
346             }
347         }
348     }
349
350     applyBatchNote() {
351         const ids = this.selectedIds();
352         if (ids.length === 0 || !this.batchNote) { return; }
353
354         this.liService.applyBatchNote(ids, this.batchNote, this.noteIsPublic)
355         .then(resp => this.load());
356     }
357
358     liPriceIsValid(li: IdlObject): boolean {
359         const price = li.estimated_unit_price();
360         if (price === null || price === undefined || price === '') {
361             return true;
362         }
363         return !Number.isNaN(Number(price)) && Number(price) >= 0;
364     }
365
366     liPriceChange(li: IdlObject) {
367         const price = li.estimated_unit_price();
368         if (this.liPriceIsValid(li)) {
369             li.estimated_unit_price(Number(price).toFixed(2));
370
371             this.net.request(
372                 'open-ils.acq',
373                 'open-ils.acq.lineitem.update',
374                 this.auth.token(), li
375             ).subscribe(resp =>
376                 this.liService.activateStateChange.emit(li.id()));
377         }
378     }
379
380     toggleShowNotes(liId: number) {
381         this.showExpandFor = null;
382         this.showNotesFor = this.showNotesFor === liId ? null : liId;
383     }
384
385     toggleShowExpand(liId: number) {
386         this.showNotesFor = null;
387         this.showExpandFor = this.showExpandFor === liId ? null : liId;
388     }
389
390     toggleExpandAll() {
391         this.showNotesFor = null;
392         this.showExpandFor = null;
393         this.expandAll = !this.expandAll;
394     }
395
396     liHasAlerts(li: IdlObject): boolean {
397         return li.lineitem_notes().filter(n => n.alert_text()).length > 0;
398     }
399
400     deleteLineitems() {
401         const ids = Object.keys(this.selected).filter(id => this.selected[id]);
402
403         const method = this.poId ?
404             'open-ils.acq.purchase_order.lineitem.delete' :
405             'open-ils.acq.picklist.lineitem.delete';
406
407         let promise = Promise.resolve();
408
409         this.loading = true;
410
411         ids.forEach(id => {
412             promise = promise
413             .then(_ => this.net.request(
414                 'open-ils.acq', method, this.auth.token(), id).toPromise()
415             );
416         });
417
418         promise.then(_ => this.load());
419     }
420
421     liHasRealCopies(li: IdlObject): boolean {
422         for (let idx = 0; idx < li.lineitem_details().length; idx++) {
423             if (li.lineitem_details()[idx].eg_copy_id()) {
424                 return true;
425             }
426         }
427         return false;
428     }
429
430     editHoldings(li: IdlObject) {
431
432         const copies = li.lineitem_details()
433             .filter(lid => lid.eg_copy_id()).map(lid => lid.eg_copy_id());
434
435         if (copies.length === 0) { return; }
436
437         this.holdings.spawnAddHoldingsUi(
438             li.eg_bib_id(),
439             copies.map(c => c.call_number()),
440             null,
441             copies.map(c => c.id())
442         );
443     }
444
445     receiveSelected() {
446         this.markReceived(this.selectedIds());
447     }
448
449     unReceiveSelected() {
450         this.markUnReceived(this.selectedIds());
451     }
452
453     cancelSelected() {
454         const liIds = this.selectedIds();
455         if (liIds.length === 0) { return; }
456
457         this.cancelDialog.open().subscribe(reason => {
458             if (!reason) { return; }
459
460             this.net.request('open-ils.acq',
461                 'open-ils.acq.lineitem.cancel.batch',
462                 this.auth.token(), liIds, reason
463             ).toPromise().then(resp => this.postBatchAction(resp, liIds));
464         });
465     }
466
467     markReceived(liIds: number[]) {
468         if (liIds.length === 0) { return; }
469
470         this.net.request(
471             'open-ils.acq',
472             'open-ils.acq.lineitem.receive.batch',
473             this.auth.token(), liIds
474         ).toPromise().then(resp => this.postBatchAction(resp, liIds));
475     }
476
477     markUnReceived(liIds: number[]) {
478         if (liIds.length === 0) { return; }
479
480         this.net.request(
481             'open-ils.acq',
482             'open-ils.acq.lineitem.receive.rollback.batch',
483             this.auth.token(), liIds
484         ).toPromise().then(resp => this.postBatchAction(resp, liIds));
485     }
486
487     postBatchAction(response: any, liIds: number[]) {
488         const evt = this.evt.parse(response);
489
490         if (evt) {
491             console.warn('Batch operation failed', evt);
492             this.batchFailure = evt;
493             return;
494         }
495
496         this.batchFailure = null;
497
498         // Remove the modified LI's from the cache so we are
499         // forced to re-fetch them.
500         liIds.forEach(id => delete this.liService.liCache[id]);
501
502         this.loadPageOfLis();
503     }
504
505     createPo(fromAll?: boolean) {
506         this.router.navigate(['/staff/acq/po/create'], {
507             queryParams: {li: fromAll ? this.lineitemIds : this.selectedIds()}
508         });
509     }
510 }
511