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';
17 templateUrl: 'lineitem-list.component.html',
18 selector: 'eg-lineitem-list',
19 styleUrls: ['lineitem-list.component.css']
21 export class LineitemListComponent implements OnInit {
23 picklistId: number = null;
27 pager: Pager = new Pager();
28 pageOfLineitems: IdlObject[] = [];
29 lineitemIds: number[] = [];
32 selected: {[id: number]: boolean} = {};
34 // Order identifier type per lineitem
35 orderIdentTypes: {[id: number]: 'isbn' | 'issn' | 'upc'} = {};
37 // Copy counts per lineitem
38 existingCopyCounts: {[id: number]: number} = {};
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[]}} = {};
46 batchSelectPage = false;
47 batchSelectAll = false;
49 showExpandFor: number; // 'Expand'
52 batchFailure: EgEvent;
55 @ViewChild('cancelDialog') cancelDialog: CancelDialogComponent;
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
70 this.route.queryParamMap.subscribe((params: ParamMap) => {
71 this.pager.offset = +params.get('offset');
72 this.pager.limit = +params.get('limit');
76 this.route.fragment.subscribe((fragment: string) => {
77 const id = Number(fragment);
78 if (id > 0) { this.focusLineitem(id); }
81 this.route.parent.paramMap.subscribe((params: ParamMap) => {
82 this.picklistId = +params.get('picklistId');
83 this.poId = +params.get('poId');
87 this.store.getItem('acq.lineitem.page_size').then(count => {
88 this.pager.setLimit(count || 20);
93 pageSizeChange(count: number) {
94 this.store.setItem('acq.lineitem.page_size', count).then(_ => {
95 this.pager.setLimit(count);
101 // Focus the selected lineitem, which may not yet exist in the
103 focusLineitem(id?: number) {
104 if (id !== undefined) { this.focusLi = id; }
106 const node = document.getElementById('' + this.focusLi);
107 if (node) { node.scrollIntoView(true); }
111 load(): Promise<any> {
112 this.pageOfLineitems = [];
115 this.pager.limit && (this.poId || this.picklistId)) {
119 return this.loadIds()
120 .then(_ => this.loadPage())
121 .then(_ => this.loading = false)
122 .catch(_ => {}); // re-route while page is loading
125 // We have not collected enough data to proceed.
126 return Promise.resolve();
130 loadIds(): Promise<any> {
131 this.lineitemIds = [];
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();
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;
145 return this.net.request(
146 'open-ils.acq', method, this.auth.token(), id, options
147 ).toPromise().then(resp => {
148 const ids = handler(resp);
150 this.lineitemIds = ids
152 .sort((id1, id2) => id1 < id2 ? -1 : 1);
154 this.pager.resultCount = ids.length;
160 this.router.navigate([], {
161 relativeTo: this.route,
162 queryParamsHandling: 'merge',
165 offset: this.pager.offset,
166 limit: this.pager.limit
171 loadPage(): Promise<any> {
172 return this.jumpToLiPage()
173 .then(_ => this.loadPageOfLis())
174 .then(_ => this.setBatchSelect())
175 .then(_ => setTimeout(() => this.focusLineitem()));
178 jumpToLiPage(): Promise<boolean> {
179 if (!this.focusLi) { return Promise.resolve(true); }
181 const idx = this.lineitemIds.indexOf(this.focusLi);
182 if (idx === -1) { return Promise.resolve(true); }
184 const offset = Math.floor(idx / this.pager.limit) * this.pager.limit;
186 return this.router.navigate(['./'], {
187 relativeTo: this.route,
188 queryParams: {offset: offset, limit: this.pager.limit},
189 fragment: '' + this.focusLi
193 loadPageOfLis(): Promise<any> {
194 this.pageOfLineitems = [];
196 const ids = this.lineitemIds.slice(
197 this.pager.offset, this.pager.offset + this.pager.limit)
198 .filter(id => id !== undefined);
200 if (ids.length === 0) { return Promise.resolve(); }
202 if (this.pageOfLineitems.length === ids.length) {
203 // All entries found in the cache
204 return Promise.resolve();
207 this.pageOfLineitems = []; // reset
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;
217 ingestOneLi(li: IdlObject, replace?: boolean) {
218 this.liMarcAttrs[li.id()] = {};
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');
227 const ident = this.liService.getOrderIdent(li);
228 this.orderIdentTypes[li.id()] = ident ? ident.attr_name() : 'isbn';
231 li.lineitem_notes(li.lineitem_notes().sort(
232 (n1, n2) => n1.create_time() < n2.create_time() ? 1 : -1));
235 for (let idx = 0; idx < this.pageOfLineitems.length; idx++) {
236 if (this.pageOfLineitems[idx].id() === li.id()) {
237 this.pageOfLineitems[idx] = li;
242 this.pageOfLineitems.push(li);
246 // First matching attr
247 displayAttr(li: IdlObject, name: string): string {
249 this.liMarcAttrs[li.id()][name] &&
250 this.liMarcAttrs[li.id()][name][0]
251 ) ? this.liMarcAttrs[li.id()][name][0].attr_value() : '';
254 // All matching attrs
255 attrs(li: IdlObject, name: string, attrType?: string): IdlObject[] {
256 return this.liService.getAttributes(li, name, attrType);
259 jacketIdent(li: IdlObject): string {
260 return this.displayAttr(li, 'isbn') || this.displayAttr(li, 'upc');
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()];
268 if (this.liMarcAttrs[li.id()][otype]) {
269 return this.liMarcAttrs[li.id()][otype].map(
270 attr => ({id: attr.id(), label: attr.attr_value()}));
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; }
282 const attr = this.identOptions(li).filter(
283 (entry: ComboboxEntry) => entry.label === ident.attr_value())[0];
284 return attr ? attr.id : null;
287 currentIdent(li: IdlObject): IdlObject {
288 return this.liService.getOrderIdent(li);
291 orderIdentChanged(li: IdlObject, entry: ComboboxEntry) {
292 if (entry === null) { return; }
294 this.liService.changeOrderIdent(
295 li, entry.id, this.orderIdentTypes[li.id()], entry.label
296 ).subscribe(freshLi => this.ingestOneLi(freshLi, true));
302 selectedIds(): number[] {
303 return Object.keys(this.selected)
304 .filter(id => this.selected[id] === true)
305 .map(id => Number(id));
309 // After a page of LI's are loaded, see if the batch-select checkbox
310 // needs to be on or off.
313 const ids = this.selectedIds();
314 this.pageOfLineitems.forEach(li => {
315 if (!ids.includes(li.id())) { on = false; }
318 this.batchSelectPage = on;
322 this.lineitemIds.forEach(id => {
323 if (!this.selected[id]) { on = false; }
326 this.batchSelectAll = on;
329 toggleSelectAll(allItems: boolean) {
332 this.lineitemIds.forEach(
333 id => this.selected[id] = this.batchSelectAll);
335 this.batchSelectPage = this.batchSelectAll;
339 this.pageOfLineitems.forEach(
340 li => this.selected[li.id()] = this.batchSelectPage);
342 if (!this.batchSelectPage) {
343 // When deselecting items in the page, we're no longer
344 // selecting all items.
345 this.batchSelectAll = false;
351 const ids = this.selectedIds();
352 if (ids.length === 0 || !this.batchNote) { return; }
354 this.liService.applyBatchNote(ids, this.batchNote, this.noteIsPublic)
355 .then(resp => this.load());
358 liPriceIsValid(li: IdlObject): boolean {
359 const price = li.estimated_unit_price();
360 if (price === null || price === undefined || price === '') {
363 return !Number.isNaN(Number(price)) && Number(price) >= 0;
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));
373 'open-ils.acq.lineitem.update',
374 this.auth.token(), li
376 this.liService.activateStateChange.emit(li.id()));
380 toggleShowNotes(liId: number) {
381 this.showExpandFor = null;
382 this.showNotesFor = this.showNotesFor === liId ? null : liId;
385 toggleShowExpand(liId: number) {
386 this.showNotesFor = null;
387 this.showExpandFor = this.showExpandFor === liId ? null : liId;
391 this.showNotesFor = null;
392 this.showExpandFor = null;
393 this.expandAll = !this.expandAll;
396 liHasAlerts(li: IdlObject): boolean {
397 return li.lineitem_notes().filter(n => n.alert_text()).length > 0;
401 const ids = Object.keys(this.selected).filter(id => this.selected[id]);
403 const method = this.poId ?
404 'open-ils.acq.purchase_order.lineitem.delete' :
405 'open-ils.acq.picklist.lineitem.delete';
407 let promise = Promise.resolve();
413 .then(_ => this.net.request(
414 'open-ils.acq', method, this.auth.token(), id).toPromise()
418 promise.then(_ => this.load());
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()) {
430 editHoldings(li: IdlObject) {
432 const copies = li.lineitem_details()
433 .filter(lid => lid.eg_copy_id()).map(lid => lid.eg_copy_id());
435 if (copies.length === 0) { return; }
437 this.holdings.spawnAddHoldingsUi(
439 copies.map(c => c.call_number()),
441 copies.map(c => c.id())
446 this.markReceived(this.selectedIds());
449 unReceiveSelected() {
450 this.markUnReceived(this.selectedIds());
454 const liIds = this.selectedIds();
455 if (liIds.length === 0) { return; }
457 this.cancelDialog.open().subscribe(reason => {
458 if (!reason) { return; }
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));
467 markReceived(liIds: number[]) {
468 if (liIds.length === 0) { return; }
472 'open-ils.acq.lineitem.receive.batch',
473 this.auth.token(), liIds
474 ).toPromise().then(resp => this.postBatchAction(resp, liIds));
477 markUnReceived(liIds: number[]) {
478 if (liIds.length === 0) { return; }
482 'open-ils.acq.lineitem.receive.rollback.batch',
483 this.auth.token(), liIds
484 ).toPromise().then(resp => this.postBatchAction(resp, liIds));
487 postBatchAction(response: any, liIds: number[]) {
488 const evt = this.evt.parse(response);
491 console.warn('Batch operation failed', evt);
492 this.batchFailure = evt;
496 this.batchFailure = null;
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]);
502 this.loadPageOfLis();
505 createPo(fromAll?: boolean) {
506 this.router.navigate(['/staff/acq/po/create'], {
507 queryParams: {li: fromAll ? this.lineitemIds : this.selectedIds()}