From 6beb3ed627d57362fd10066531b94c9a2aca9c92 Mon Sep 17 00:00:00 2001 From: Galen Charlton Date: Tue, 3 Mar 2020 18:18:55 -0500 Subject: [PATCH] ACQPRO final squash ACQPRO squash #11 ACQPRO squash #10 ang providers: create base modules and components acq providers: allow search form and details view to share a tab start work on AcqProviderSearch ang provider search form component fix thinko more work on hooking up search service and search form TODO: - contact name searching - get the org-family-selector to show the starting value of the WS OU - refactor how form parameters are managed? WIP: complex, cross-table searches Require classes for search; be very verbose for typescript make ng lint happier toward summary pane add summary pane component to module and a missing decorator initial stab at provider record service set active provider ID when a row is selected Note - not changing the route for now address and contact stubs actually push out initial version of provider record service hook up contact and provider components to record service add contact and address components to their tabs refresh record service upon ID change provider address modal provider record service updates - fetch() now returns a promise - added refreshCurrent(); - added batchUpdate() update provider address component to reflect updates to provider record service ACQPRO squash #8 default to valid == true when creating new provider addresses contacts modal update provider contacts component to reflect updates in provider record service provider record service: add currentProviderId This works around some race conditions fetching the current provider upon initialization; we should fix this so that current() can account for in-flight initialization or refreshes. add provider invoices component provider purchase orders component ACQPRO squash #7 hide provider column by default in contact and address grids... ... but if the user insists on displaying that column, redundantly, flesh it so that the code is displayed move provider search form The provider search form is now outside of the tabset and its visibility is controlled by a button. Also, clicking on a row on the search results will now hide the search form. after selecting a provider from the search form, update the route hide the provider info tabset if there's no selected provider add a bit of separation anq holdings component fix adding new holdings subfield mappings ACQPRO squash #12 new provder modal start work on provider details tab TODO: teach the record editor how to do a two-column layout TODO: add delete button Attributes tab XXX: Stub function to allow compilation toggle for provider summary pane stretch provider UI when summary is collapsed also hide Delete Provider button when collapsed to give more opportunity for stretch use materials icons expand_less/expand_more contact addresses component ACQPRO squash #6 ensure that provider contact addresses component gets found before reloading its grid Refresh contact addresses from updated service data basic EDI account management update summary after editing provider add grid for displaying EDI messages for a selected account adjust display of URL in summary pane * hyperlink URL * do not include an empty anchor if no URL is supplied summary: display active flag using eg-bool always pass through the state of the 'active' field on the search form add placeholders for the eg-comboboxes on the search form tweak display of contact role in sidebar add field help to the EDI account modal add a route resolver to ensure that the provider has been fetched when linking by ID update summary when contacts are edited Make provider addresses, contacts, and contact addresses filterable add 'View EDI Messages' grid toolbar button implement set default view button fix display provider name and code even when summary is hidden use experimental remain open on error ACQPRO squash #5 WIP: edit tag on holdings tab Alphabetize the Holdings Definition dropdown (32) Relabel holdings definitions for consistency (26) Fix thinko to allow Currency Type searching (30) Direct provider (and shipper) links in search and provider interfaces at the new Angular UI (24) Fix thinko with non-pcrud grid paging (31) adjust links to providers so that user-specified default tab is displayed add human-friendly labels for the name column in the holdings definitions grid improve handling of currency type (LH#30) - display currency label in the summary - fix ability to edit it in the details fm-editor - preload the currency drop-down improve display of the EDI Default field (LH#23) flesh provider in the provider EDI account grid (LH#23) (though this is a bit redundant since the list of EDI accounts displayed is restricted to the ones owned by the current provider) add grid config to ang ACQPRO squash #4 tweak columns in addresses tab (LH#25) - set a default column order - don't display the ID column by default ACQPRO squash #3 tweak provider contact addresses Following LH#25 change input for Active? on search form to a tri-value select (LH#16) fix styling of clear form button ACQPRO squash #2 sort by provider name by default (LH#14) submit search form immediately (LH#14) TODO: see if we can defer this if all we've done is gone directly to a specific provider immediately run search only of search form starts off being visble (LH#14) LH#38: add mechanism to detect if provider can be deleted LH#6: change "Clear Form" => "Reset Form" LH#41: add column to EDI account table to indicate which account is the provider's default LH#12 various styling improvements - clean up how the hide search form and new provider column flows - add a bit of padding below the Set Default View button - change the label of the Owning Library search input to Owner - keep the Owner label and OU search input on same line for narrower screens LH#8 turn on asyncSupportsEmptyTermClick for some fields in search form Specifically: EDI Default and Currency LH#8 preload EDI default default claim policy on provider create/update forms LH#1: match responsive breakpoint of summary pane and main page LH#1 tweaks to display of search form and summary - search form now displays above the summary and vendor - Delete Provider button now only displayed where there is an active provider - no traces of the summary pane appear when entering the interface fresh improve record retrieval and refreshes - reduce the number of redundant fetches - use subscriptions to inform child components when they need to refresh themselves update PO and LI components to use AttrDefsService wire up functionality for primary contact don't show the primary contact field on the create modal send toast and navigate away upon deleting provider quell some console noise clear search form and redo default search if same-URL-navigated account for staff users who do not have the ADMIN_PROVIDER permission TODO: make the Delete Provider and New Provider buttons be deactivated or not appear as relevant. LH#15: tweaks to primary contact functionality - added unset as primary action - fixed confirmation modals - more selectively enable and disable the set and unset actions LH#9: tweak click actions on provider results form - single click: retrieve provider but do not hide search form - double click: retrieve provider and hide search form LH#9: add explicit 'Retrieve Provider' action LH#4: add sticky expand/collapse of search form LH#21: tweak holding tag form LH#17: warn if attempting to leave provider edits in flight acqpro SQUASH #1 avoid glitch in low-permission mode more tweaks for low permissions scenarios LH#29: ensure that we don't search for providers that we can't retrieve handle permissions coming in late LH#27: navigate to search page if given direct link to provider that cannot be retrieved --- .../acq-provider-search-form.component.css | 5 + .../acq-provider-search-form.component.html | 64 ++++ .../provider/acq-provider-search-form.component.ts | 134 +++++++++ .../acq/provider/acq-provider-search.service.ts | 205 +++++++++++++ .../staff/acq/provider/acq-provider.component.html | 152 ++++++++++ .../staff/acq/provider/acq-provider.component.ts | 192 ++++++++++++ .../app/staff/acq/provider/acq-provider.module.ts | 50 ++++ .../acq/provider/provider-addresses.component.html | 39 +++ .../acq/provider/provider-addresses.component.ts | 236 +++++++++++++++ .../provider/provider-attributes.component.html | 26 ++ .../acq/provider/provider-attributes.component.ts | 190 ++++++++++++ .../provider-contact-addresses.component.html | 39 +++ .../provider-contact-addresses.component.ts | 229 +++++++++++++++ .../acq/provider/provider-contacts.component.html | 68 +++++ .../acq/provider/provider-contacts.component.ts | 325 +++++++++++++++++++++ .../acq/provider/provider-details.component.html | 17 ++ .../acq/provider/provider-details.component.ts | 82 ++++++ .../provider/provider-edi-accounts.component.html | 74 +++++ .../provider/provider-edi-accounts.component.ts | 281 ++++++++++++++++++ .../acq/provider/provider-holdings.component.html | 83 ++++++ .../acq/provider/provider-holdings.component.ts | 223 ++++++++++++++ .../acq/provider/provider-invoices.component.html | 45 +++ .../acq/provider/provider-invoices.component.ts | 120 ++++++++ .../provider-purchase-orders.component.html | 38 +++ .../provider/provider-purchase-orders.component.ts | 89 ++++++ .../staff/acq/provider/provider-record.service.ts | 198 +++++++++++++ .../acq/provider/provider-results.component.html | 25 ++ .../acq/provider/provider-results.component.ts | 83 ++++++ .../src/app/staff/acq/provider/resolver.service.ts | 57 ++++ .../src/app/staff/acq/provider/routing.module.ts | 30 ++ .../staff/acq/provider/summary-pane.component.css | 27 ++ .../staff/acq/provider/summary-pane.component.html | 125 ++++++++ .../staff/acq/provider/summary-pane.component.ts | 211 +++++++++++++ .../src/eg2/src/app/staff/acq/routing.module.ts | 4 + .../acq/search/invoice-results.component.html | 4 +- .../acq/search/lineitem-results.component.html | 2 +- .../search/purchase-order-results.component.html | 2 +- 37 files changed, 3770 insertions(+), 4 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider-search-form.component.css create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider-search-form.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider-search-form.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider-search.service.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider.module.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/provider-addresses.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/provider-addresses.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/provider-attributes.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/provider-attributes.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/provider-contact-addresses.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/provider-contact-addresses.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/provider-contacts.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/provider-contacts.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/provider-details.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/provider-details.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/provider-edi-accounts.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/provider-edi-accounts.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/provider-holdings.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/provider-holdings.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/provider-invoices.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/provider-invoices.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/provider-purchase-orders.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/provider-purchase-orders.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/provider-record.service.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/provider-results.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/provider-results.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/resolver.service.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/routing.module.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/summary-pane.component.css create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/summary-pane.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/acq/provider/summary-pane.component.ts diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider-search-form.component.css b/Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider-search-form.component.css new file mode 100644 index 0000000000..435a067e91 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider-search-form.component.css @@ -0,0 +1,5 @@ +#acq-provider-search-form { + border-radius: 0px 0px 7px 7px; + background-color: rgb(247, 247, 247); + box-shadow: 1px 2px 3px -1px rgba(0, 0, 0, .2); +} diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider-search-form.component.html b/Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider-search-form.component.html new file mode 100644 index 0000000000..1dbe407789 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider-search-form.component.html @@ -0,0 +1,64 @@ +
+
+
+
+ +
+
+ +
+
+ + +
+
+ + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider-search-form.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider-search-form.component.ts new file mode 100644 index 0000000000..618cb44b55 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider-search-form.component.ts @@ -0,0 +1,134 @@ +import {Component, OnInit, AfterViewInit, Input, Output, EventEmitter, ViewChild} from '@angular/core'; +import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap'; +import {Router, ActivatedRoute} from '@angular/router'; +import {StaffCommonModule} from '@eg/staff/common.module'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {AuthService} from '@eg/core/auth.service'; +import {AcqProviderSearchTerm, AcqProviderSearch} from './acq-provider-search.service'; +import {StoreService} from '@eg/core/store.service'; +import {OrgFamily} from '@eg/share/org-family-select/org-family-select.component'; + +@Component({ + selector: 'eg-acq-provider-search-form', + styleUrls: ['acq-provider-search-form.component.css'], + templateUrl: './acq-provider-search-form.component.html' +}) + +export class AcqProviderSearchFormComponent implements OnInit, AfterViewInit { + + @Output() searchSubmitted = new EventEmitter(); + + collapsed = false; + + providerName = ''; + providerCode = ''; + providerOwners: OrgFamily; + contactName = ''; + providerEmail = ''; + providerPhone = ''; + providerCurrencyType = ''; + providerSAN = ''; + providerEDIDefault = null; + providerURL = ''; + providerIsActive = 'active'; + + constructor( + private router: Router, + private route: ActivatedRoute, + private pcrud: PcrudService, + private localStore: StoreService, + private idl: IdlService, + private toast: ToastService, + private auth: AuthService, + ) {} + + ngOnInit() { + const self = this; + this.providerOwners = {primaryOrgId: this.auth.user().ws_ou(), includeDescendants: true}; + this.collapsed = this.localStore.getLocalItem('eg.acq.provider.search.collapse_form') || false; + } + + ngAfterViewInit() {} + + clearSearch() { + this.providerName = ''; + this.providerCode = ''; + this.providerOwners = {primaryOrgId: this.auth.user().ws_ou(), includeDescendants: true}; + this.contactName = ''; + this.providerEmail = ''; + this.providerPhone = ''; + this.providerCurrencyType = ''; + this.providerSAN = ''; + this.providerEDIDefault = null; + this.providerURL = ''; + this.providerIsActive = 'active'; + } + + submitSearch() { + + const searchTerms: AcqProviderSearchTerm[] = []; + if (this.providerName) { + searchTerms.push({ classes: ['acqpro'], fields: ['name'], op: 'ilike', value: this.providerName }); + } + if (this.providerCode) { + searchTerms.push({ classes: ['acqpro'], fields: ['code'], op: 'ilike', value: this.providerCode }); + } + if (this.providerOwners) { + searchTerms.push({ classes: ['acqpro'], fields: ['owner'], op: 'in', value: this.providerOwners.orgIds }); + } + if (this.contactName) { + searchTerms.push({ classes: ['acqpc'], fields: ['name'], op: 'ilike', value: this.contactName }); + } + if (this.providerEmail) { + searchTerms.push({ classes: ['acqpro', 'acqpc'], fields: ['email', 'email'], op: 'ilike', value: this.providerEmail }); + } + if (this.providerPhone) { + // this requires the flesh hash to contain: { ... join: { acqpc: { type: 'left' } } ... } + searchTerms.push({ + classes: ['acqpc', 'acqpro', 'acqpro'], + fields: ['phone', 'phone', 'fax_phone'], + op: 'ilike', + value: this.providerPhone, + }); + } + if (this.providerCurrencyType) { + searchTerms.push({ classes: ['acqpro'], fields: ['currency_type'], op: '=', value: this.providerCurrencyType }); + } + if (this.providerSAN) { + searchTerms.push({ classes: ['acqpro'], fields: ['san'], op: 'ilike', value: this.providerSAN }); + } + if (this.providerEDIDefault) { + searchTerms.push({ classes: ['acqpro'], fields: ['edi_default'], op: '=', value: this.providerEDIDefault }); + } + if (this.providerURL) { + searchTerms.push({ classes: ['acqpro'], fields: ['url'], op: 'ilike', value: this.providerURL }); + } + switch (this.providerIsActive) { + case 'active': { + searchTerms.push({ classes: ['acqpro'], fields: ['active'], op: '=', value: 't' }); + break; + } + case 'inactive': { + searchTerms.push({ classes: ['acqpro'], fields: ['active'], op: '=', value: 'f' }); + break; + } + } + + // tossing setTimeout here to ensure that the + // grid data source is fully initialized + setTimeout(() => { + this.searchSubmitted.emit({ + terms: searchTerms, + }); + }); + } + + toggleCollapse() { + this.collapsed = ! this.collapsed; + this.localStore.setLocalItem('eg.acq.provider.search.collapse_form', this.collapsed); + } + +} diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider-search.service.ts b/Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider-search.service.ts new file mode 100644 index 0000000000..38b61d3814 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider-search.service.ts @@ -0,0 +1,205 @@ +import {Injectable} from '@angular/core'; +import {empty, throwError} from 'rxjs'; +import {map} from 'rxjs/operators'; +import {AuthService} from '@eg/core/auth.service'; +import {GridDataSource} from '@eg/share/grid/grid'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {Pager} from '@eg/share/util/pager'; +import {EventService} from '@eg/core/event.service'; +import {ProviderRecordService} from './provider-record.service'; + +export interface AcqProviderSearchTerm { + classes: string[]; + fields: string[]; + op: string; + value: any; +} + +export interface AcqProviderSearch { + terms: AcqProviderSearchTerm[]; +} + +@Injectable() +export class AcqProviderSearchService { + + _terms: AcqProviderSearchTerm[] = []; + firstRun = true; + + constructor( + private evt: EventService, + private auth: AuthService, + private pcrud: PcrudService, + private providerRecord: ProviderRecordService + ) { + this.firstRun = true; + } + + setSearch(search: AcqProviderSearch) { + this._terms = search.terms; + this.firstRun = false; + } + + generateSearchJoins(): any { + const joinPart = new Object(); + let class_list = new Array(); + + // get all the classes used + this._terms.forEach(term => { class_list = class_list.concat(term.classes); }); + + // filter out acqpro, empty names, and make unique + class_list = class_list.filter((x, i, a) => x && x !== 'acqpro' && a.indexOf(x) === i); + + // build a join clause for use in the "opts" part of a pcrud query + class_list.forEach(cls => { joinPart[cls] = {type : 'left' }; }); + + if (Object.keys(joinPart).length === 0) { return null; } + return joinPart; + } + + generateSearch(filters): any { + // base query to grab all providers + const base = { id: { '!=': null } }; + const query: any = new Array(); + query.push(base); + + // handle supplied search terms + this._terms.forEach(term => { + if (term.value === '') { + return; + } + + // not const because we may want an array + let query_obj = new Object(); + const query_arr = new Array(); + + let op = term.op; + if (!op) { op = '='; } // just in case + + let val = term.value; + if (op === 'ilike') { + val = '%' + val + '%'; + } + + let isOR = false; + term.fields.forEach( (field, ind) => { + const curr_cls = term.classes[ind]; + + // remove any OUs that the user doesn't have provider view + // permission for + if (curr_cls === 'acqpro' && field === 'owner' && op === 'in') { + val = val.filter(ou => { + return this.providerRecord.getViewOUs().includes(ou); + }); + } + + if (ind === 1) { + // we're OR'ing in other classes/fields + // and this is the first so restructure + const first_cls = term.classes[0]; + isOR = true; + let tmp = new Object(); + if (first_cls) { + tmp['+' + first_cls] = query_obj; + } else { + tmp = query_obj; + } + + query_arr.push(tmp); + } + + if (curr_cls) { + if (isOR) { + const tmp = new Object(); + tmp['+' + curr_cls] = new Object(); + tmp['+' + curr_cls][field] = new Object(); + tmp['+' + curr_cls][field][op] = val; + query_arr.push(tmp); + } else { + query_obj['+' + curr_cls] = new Object(); + query_obj['+' + curr_cls][field] = new Object(); + query_obj['+' + curr_cls][field][op] = val; + } + } else { + if (isOR) { + const tmp = new Object(); + tmp[field] = new Object(); + tmp[field][op] = val; + query_arr.push(tmp); + } else { + query_obj[field] = new Object(); + query_obj[field][op] = val; + } + } + + }); + + if (isOR) { query_obj = {'-or': query_arr}; } + query.push(query_obj); + }); + + // handle grid filters + // note that date filters coming from the grid do not need + // to worry about __castdate because the grid filter supplies + // both the start and end times + const observables = []; + Object.keys(filters).forEach(filterField => { + filters[filterField].forEach(condition => { + query.push(condition); + }); + }); + return query; + } + + getDataSource(): GridDataSource { + const gridSource = new GridDataSource(); + + // we'll sort by provder name by default + gridSource.sort = [{ name: 'name', dir: 'ASC' }]; + + gridSource.getRows = (pager: Pager, sort: any[]) => { + + // don't do a search the very first time we + // get invoked, which is during initialization; we'll + // let components higher up the change decide whether + // to submit a search + if (this.firstRun) { + this.firstRun = false; + return empty(); + } + + const joins = this.generateSearchJoins(); + const query = this.generateSearch(gridSource.filters); + + const opts = {}; + if (joins) { opts['join'] = joins; } + opts['offset'] = pager.offset; + opts['limit'] = pager.limit; + opts['au_by_id'] = true; + + if (sort.length > 0) { + opts['order_by'] = []; + sort.forEach(sort_clause => { + opts['order_by'].push({ + class: 'acqpro', + field: sort_clause.name, + direction: sort_clause.dir + }); + }); + } + + return this.pcrud.search('acqpro', + query, + opts + ).pipe( + map(res => { + if (this.evt.parse(res)) { + throw throwError(res); + } else { + return res; + } + }), + ); + }; + return gridSource; + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider.component.html b/Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider.component.html new file mode 100644 index 0000000000..d4fa8d1de2 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider.component.html @@ -0,0 +1,152 @@ + + + + + + + + +
+ +
+
+
+

{{providerRecord.currentProvider?.record.name()}} ({{providerRecord.currentProvider?.record.code()}})

+
+
+ +
+
+
+
+ +
+
+ +

{{providerRecord.currentProvider?.record.name()}} ({{providerRecord.currentProvider?.record.code()}})

+
+
+ + +
+ +
+
+
+ + + +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+
+
+
+
+
+
+ +
+ + + diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider.component.ts new file mode 100644 index 0000000000..7771c970f7 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider.component.ts @@ -0,0 +1,192 @@ +import {Component, OnInit, AfterViewInit, ViewChild, ChangeDetectorRef, OnDestroy} from '@angular/core'; +import {filter, takeUntil} from 'rxjs/operators'; +import {Subject, Observable, of} from 'rxjs'; +import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap'; +import {Router, ActivatedRoute, ParamMap, RouterEvent, NavigationEnd} from '@angular/router'; +import {StaffCommonModule} from '@eg/staff/common.module'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {AcqProviderSummaryPaneComponent} from './summary-pane.component'; +import {ProviderDetailsComponent} from './provider-details.component'; +import {ProviderHoldingsComponent} from './provider-holdings.component'; +import {ProviderResultsComponent} from './provider-results.component'; +import {ProviderRecordService} from './provider-record.service'; +import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {AuthService} from '@eg/core/auth.service'; +import {StoreService} from '@eg/core/store.service'; +import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component'; + +@Component({ + templateUrl: './acq-provider.component.html' +}) + +export class AcqProviderComponent implements OnInit, AfterViewInit, OnDestroy { + + activeTab = ''; + showSearchForm = false; + id = null; + validTabTypes = ['details', 'addresses', 'contacts', 'attributes', 'holdings', 'edi_accounts', 'purchase_orders', 'invoices']; + defaultTabType = 'details'; + @ViewChild('acqSearchProviderSummary', { static: true }) providerSummaryPane: AcqProviderSummaryPaneComponent; + @ViewChild('acqProviderResults', { static: true }) acqProviderResults: ProviderResultsComponent; + @ViewChild('providerDetails', { static: false }) providerDetails: ProviderDetailsComponent; + @ViewChild('providerHoldings', { static: false }) providerHoldings: ProviderHoldingsComponent; + @ViewChild('createDialog', { static: true }) createDialog: FmRecordEditorComponent; + @ViewChild('createString', { static: false }) createString: StringComponent; + @ViewChild('createErrString', { static: false }) createErrString: StringComponent; + @ViewChild('leaveConfirm', { static: true }) leaveConfirm: ConfirmDialogComponent; + + onTabChange: ($event: NgbTabChangeEvent) => void; + + onDesireSummarize: ($event: number, updateSummaryOnly?: boolean) => void; + onSummaryToggled: ($event: boolean) => void; + + previousUrl: string = null; + public destroyed = new Subject(); + _alreadyDeactivated = false; + + constructor( + private router: Router, + private route: ActivatedRoute, + private auth: AuthService, + private pcrud: PcrudService, + private idl: IdlService, + private providerRecord: ProviderRecordService, + private toast: ToastService, + private store: StoreService, + private changeDetector: ChangeDetectorRef + ) { + this.router.events.pipe( + filter((event: RouterEvent) => event instanceof NavigationEnd), + takeUntil(this.destroyed) + ).subscribe(routeEvent => { + if (routeEvent instanceof NavigationEnd) { + if (this.previousUrl != null && + routeEvent.url === '/staff/acq/provider' && + this.previousUrl === routeEvent.url) { + this.acqProviderResults.resetSearch(); + } + this.previousUrl = routeEvent.url; + } + }); + } + + ngOnInit() { + const self = this; + + const tabTypeParam = this.route.snapshot.paramMap.get('tab'); + const idParam = this.route.snapshot.paramMap.get('id'); + + this.defaultTabType = + this.store.getLocalItem('eg.acq.provider.default_tab') || 'details'; + + if (idParam) { + this.showSearchForm = false; + this.id = idParam; + if (!tabTypeParam) { + this.activeTab = this.defaultTabType; + this.router.navigate(['/staff', 'acq', 'provider', this.id, this.activeTab]); + } + } + + if (tabTypeParam) { + this.showSearchForm = false; + if (this.validTabTypes.includes(tabTypeParam)) { + this.activeTab = tabTypeParam; + } else { + this.activeTab = this.defaultTabType; + this.router.navigate(['/staff', 'acq', 'provider', this.id, this.activeTab]); + } + } else { + this.showSearchForm = true; + } + + this.onTabChange = ($event) => { + $event.preventDefault(); + this.canDeactivate().subscribe(canLeave => { + if (!canLeave) { return; } + this._alreadyDeactivated = true; // don't trigger again on the route change + if (this.validTabTypes.includes($event.nextId)) { + this.activeTab = $event.nextId; + const id = this.route.snapshot.paramMap.get('id'); + this.router.navigate(['/staff', 'acq', 'provider', this.id, $event.nextId]); + } + }); + }; + + this.onDesireSummarize = ($event, updateSummaryOnly = false) => { + this.id = $event; + this.providerRecord.fetch(this.id).then(() => { + // $event is a provider ID + this.providerSummaryPane.update($event); + if (this.providerDetails) { + this.providerDetails.refresh(); + } + if (updateSummaryOnly) { + return; + } + this.providerRecord.announceProviderUpdated(); + this.showSearchForm = false; + this.activeTab = this.defaultTabType; + this.router.navigate(['/staff', 'acq', 'provider', this.id, this.activeTab]); + }); + }; + + this.onSummaryToggled = ($event) => { + // in case this is useful for a better implementation of reflowing the UI + }; + } + + ngAfterViewInit() { + this.changeDetector.detectChanges(); + } + + ngOnDestroy(): void { + this.destroyed.next(); + this.destroyed.complete(); + } + + setDefaultTab() { + this.defaultTabType = this.activeTab; + this.store.setLocalItem('eg.acq.provider.default_tab', this.activeTab); + } + + createNew() { + this.createDialog.mode = 'create'; + const provider = this.idl.create('acqpro'); + provider.active(true); + provider.owner(this.auth.user().ws_ou()); + provider.default_copy_count(1); + this.createDialog.record = provider; + this.createDialog.recordId = null; + this.createDialog.open({size: 'lg'}).subscribe( + ok => { + this.createString.current() + .then(str => this.toast.success(str)); + this.onDesireSummarize(ok.id()); + }, + rejection => { + if (!rejection.dismissed) { + this.createErrString.current() + .then(str => this.toast.danger(str)); + } + } + ); + } + + canDeactivate(): Observable { + if (this._alreadyDeactivated) { + // one freebie + this._alreadyDeactivated = false; + return of(true); + } + if ((this.providerHoldings && this.providerHoldings.isDirty()) || + (this.providerDetails && this.providerDetails.isDirty())) { + return this.leaveConfirm.open(); + } else { + return of(true); + } + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider.module.ts b/Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider.module.ts new file mode 100644 index 0000000000..cbfd46d9d0 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider.module.ts @@ -0,0 +1,50 @@ +import {NgModule} from '@angular/core'; +import {StaffCommonModule} from '@eg/staff/common.module'; +import {AcqProviderRoutingModule} from './routing.module'; +import {AcqProviderComponent} from './acq-provider.component'; +import {AcqProviderSearchFormComponent} from './acq-provider-search-form.component'; +import {AcqProviderSummaryPaneComponent} from './summary-pane.component'; +import {ProviderResultsComponent} from './provider-results.component'; +import {ProviderDetailsComponent} from './provider-details.component'; +import {ProviderAddressesComponent} from './provider-addresses.component'; +import {ProviderContactsComponent} from './provider-contacts.component'; +import {ProviderContactAddressesComponent} from './provider-contact-addresses.component'; +import {ProviderHoldingsComponent} from './provider-holdings.component'; +import {ProviderAttributesComponent} from './provider-attributes.component'; +import {ProviderEdiAccountsComponent} from './provider-edi-accounts.component'; +import {ProviderInvoicesComponent} from './provider-invoices.component'; +import {ProviderPurchaseOrdersComponent} from './provider-purchase-orders.component'; +import {OrgFamilySelectModule} from '@eg/share/org-family-select/org-family-select.module'; +import {FmRecordEditorModule} from '@eg/share/fm-editor/fm-editor.module'; +import {ProviderRecordService} from './provider-record.service'; + +@NgModule({ + declarations: [ + AcqProviderComponent, + AcqProviderSearchFormComponent, + AcqProviderSummaryPaneComponent, + ProviderResultsComponent, + ProviderDetailsComponent, + ProviderAddressesComponent, + ProviderContactsComponent, + ProviderContactAddressesComponent, + ProviderHoldingsComponent, + ProviderAttributesComponent, + ProviderEdiAccountsComponent, + ProviderInvoicesComponent, + ProviderPurchaseOrdersComponent, + AcqProviderSummaryPaneComponent + ], + imports: [ + StaffCommonModule, + OrgFamilySelectModule, + FmRecordEditorModule, + AcqProviderRoutingModule + ], + providers: [ + ProviderRecordService + ], +}) + +export class AcqProviderModule { +} diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-addresses.component.html b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-addresses.component.html new file mode 100644 index 0000000000..e64f98df6b --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-addresses.component.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-addresses.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-addresses.component.ts new file mode 100644 index 0000000000..08eb4b264d --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-addresses.component.ts @@ -0,0 +1,236 @@ +import {Component, OnInit, AfterViewInit, OnDestroy, Input, ViewChild} from '@angular/core'; +import {empty, throwError, Observable, from, Subscription} from 'rxjs'; +import {map} from 'rxjs/operators'; +import {Router, ActivatedRoute, ParamMap} from '@angular/router'; +import {Pager} from '@eg/share/util/pager'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {NetService} from '@eg/core/net.service'; +import {AuthService} from '@eg/core/auth.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {EventService} from '@eg/core/event.service'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid'; +import {ProviderRecord, ProviderRecordService} from './provider-record.service'; +import {AcqProviderSearchFormComponent} from './acq-provider-search-form.component'; +import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ToastService} from '@eg/share/toast/toast.service'; + +@Component({ + selector: 'eg-provider-addresses', + templateUrl: 'provider-addresses.component.html', +}) +export class ProviderAddressesComponent implements OnInit, AfterViewInit, OnDestroy { + + addresses: any[] = []; + + gridSource: GridDataSource; + @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent; + @ViewChild('acqProviderAddressesGrid', { static: true }) providerAddressesGrid: GridComponent; + @ViewChild('successString', { static: true }) successString: StringComponent; + @ViewChild('createString', { static: false }) createString: StringComponent; + @ViewChild('createErrString', { static: false }) createErrString: StringComponent; + @ViewChild('updateFailedString', { static: false }) updateFailedString: StringComponent; + @ViewChild('deleteFailedString', { static: true }) deleteFailedString: StringComponent; + @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent; + + cellTextGenerator: GridCellTextGenerator; + provider: IdlObject; + + canCreate: boolean; + canDelete: boolean; + deleteSelected: (rows: IdlObject[]) => void; + + permissions: {[name: string]: boolean}; + + subscription: Subscription; + + // Size of create/edito dialog. Uses large by default. + @Input() dialogSize: 'sm' | 'lg' = 'lg'; + + constructor( + private router: Router, + private route: ActivatedRoute, + private net: NetService, + private evt: EventService, + private pcrud: PcrudService, + private idl: IdlService, + private auth: AuthService, + private providerRecord: ProviderRecordService, + private toast: ToastService) { + } + + ngOnInit() { + this.gridSource = this.getDataSource(); + this.cellTextGenerator = {}; + this.deleteSelected = (idlThings: IdlObject[]) => { + idlThings.forEach(idlThing => idlThing.isdeleted(true)); + this.providerRecord.batchUpdate(idlThings).subscribe( + val => { + console.debug('deleted: ' + val); + this.deleteSuccessString.current() + .then(str => this.toast.success(str)); + }, + err => { + this.deleteFailedString.current() + .then(str => this.toast.danger(str)); + }, + () => { + this.providerRecord.refreshCurrent().then( + () => this.providerAddressesGrid.reload() + ); + } + ); + }; + this.providerAddressesGrid.onRowActivate.subscribe( + (idlThing: IdlObject) => this.showEditDialog(idlThing) + ); + this.subscription = this.providerRecord.providerUpdated$.subscribe( + id => { + this.providerAddressesGrid.reload(); + } + ); + } + + ngAfterViewInit() { + console.log('this.providerRecord', this.providerRecord); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + + generateSearch(filters): any { + const query: any = new Array(); + + Object.keys(filters).forEach(filterField => { + filters[filterField].forEach(condition => { + query.push(condition); + }); + }); + return query; + } + + getDataSource(): GridDataSource { + const gridSource = new GridDataSource(); + + gridSource.getRows = (pager: Pager, sort: any[]) => { + this.provider = this.providerRecord.current(); + if (!this.provider) { + return empty(); + } + let addresses = this.provider.addresses(); + + const query = this.generateSearch(gridSource.filters); + if (query.length) { + query.unshift( { id: addresses.map(a => a.id()) } ); + + const opts = {}; + opts['offset'] = pager.offset; + opts['limit'] = pager.limit; + opts['au_by_id'] = true; + + if (sort.length > 0) { + opts['order_by'] = []; + sort.forEach(sort_clause => { + opts['order_by'].push({ + class: 'acqpa', + field: sort_clause.name, + direction: sort_clause.dir + }); + }); + } + + return this.pcrud.search('acqpa', + query, + opts + ).pipe( + map(res => { + if (this.evt.parse(res)) { + throw throwError(res); + } else { + return res; + } + }), + ); + } + + if (sort.length > 0) { + addresses = addresses.sort((a, b) => { + for (let i = 0; i < sort.length; i++) { + let lt = -1; + const sfield = sort[i].name; + if (sort[i].dir.substring(0, 1).toLowerCase() === 'd') { + lt *= -1; + } + if (a[sfield]() < b[sfield]()) { return lt; } + if (a[sfield]() > b[sfield]()) { return lt * -1; } + } + return 0; + }); + + } + + return from(addresses.slice(pager.offset, pager.offset + pager.limit)); + }; + return gridSource; + } + + showEditDialog(providerAddress: IdlObject): Promise { + this.editDialog.mode = 'update'; + this.editDialog.recordId = providerAddress['id'](); + return new Promise((resolve, reject) => { + this.editDialog.open({size: this.dialogSize}).subscribe( + result => { + this.successString.current() + .then(str => this.toast.success(str)); + this.providerRecord.refreshCurrent().then( + () => this.providerAddressesGrid.reload() + ); + resolve(result); + }, + error => { + this.updateFailedString.current() + .then(str => this.toast.danger(str)); + reject(error); + } + ); + }); + } + + editSelected(providerAddressFields: IdlObject[]) { + // Edit each IDL thing one at a time + const editOneThing = (providerAddress: IdlObject) => { + if (!providerAddress) { return; } + + this.showEditDialog(providerAddress).then( + () => editOneThing(providerAddressFields.shift())); + }; + + editOneThing(providerAddressFields.shift()); + } + + createNew() { + this.editDialog.mode = 'create'; + const address = this.idl.create('acqpa'); + address.provider(this.provider.id()); + address.valid(true); + this.editDialog.record = address; + this.editDialog.recordId = null; + this.editDialog.open({size: this.dialogSize}).subscribe( + ok => { + this.createString.current() + .then(str => this.toast.success(str)); + this.providerRecord.refreshCurrent().then( + () => this.providerAddressesGrid.reload() + ); + }, + rejection => { + if (!rejection.dismissed) { + this.createErrString.current() + .then(str => this.toast.danger(str)); + } + } + ); + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-attributes.component.html b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-attributes.component.html new file mode 100644 index 0000000000..a728c410cf --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-attributes.component.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-attributes.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-attributes.component.ts new file mode 100644 index 0000000000..c02955b117 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-attributes.component.ts @@ -0,0 +1,190 @@ +import {Component, OnInit, AfterViewInit, OnDestroy, Input, ViewChild} from '@angular/core'; +import {empty, throwError, Observable, from, Subscription} from 'rxjs'; +import {map} from 'rxjs/operators'; +import {Router, ActivatedRoute, ParamMap} from '@angular/router'; +import {Pager} from '@eg/share/util/pager'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {NetService} from '@eg/core/net.service'; +import {AuthService} from '@eg/core/auth.service'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid'; +import {ProviderRecordService} from './provider-record.service'; +import {AcqProviderSearchFormComponent} from './acq-provider-search-form.component'; +import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ToastService} from '@eg/share/toast/toast.service'; + + +@Component({ + selector: 'eg-provider-attributes', + templateUrl: 'provider-attributes.component.html', +}) +export class ProviderAttributesComponent implements OnInit, AfterViewInit, OnDestroy { + + @Input() providerId: any; + attributes: any[] = []; + + gridSource: GridDataSource; + @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent; + @ViewChild('acqProviderAttributesGrid', { static: true }) providerAttributesGrid: GridComponent; + @ViewChild('successString', { static: true }) successString: StringComponent; + @ViewChild('createString', { static: false }) createString: StringComponent; + @ViewChild('createErrString', { static: false }) createErrString: StringComponent; + @ViewChild('updateFailedString', { static: false }) updateFailedString: StringComponent; + @ViewChild('deleteFailedString', { static: true }) deleteFailedString: StringComponent; + @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent; + + cellTextGenerator: GridCellTextGenerator; + provider: IdlObject; + + canCreate: boolean; + canDelete: boolean; + deleteSelected: (rows: IdlObject[]) => void; + + permissions: {[name: string]: boolean}; + + subscription: Subscription; + + // Size of create/edito dialog. Uses large by default. + @Input() dialogSize: 'sm' | 'lg' = 'lg'; + + constructor( + private router: Router, + private route: ActivatedRoute, + private net: NetService, + private auth: AuthService, + private idl: IdlService, + private providerRecord: ProviderRecordService, + private toast: ToastService) { + + } + + ngOnInit() { + this.gridSource = this.getDataSource(); + this.cellTextGenerator = {}; + this.deleteSelected = (idlThings: IdlObject[]) => { + idlThings.forEach(idlThing => idlThing.isdeleted(true)); + this.providerRecord.batchUpdate(idlThings).subscribe( + val => { + console.debug('deleted: ' + val); + this.deleteSuccessString.current() + .then(str => this.toast.success(str)); + }, + err => { + this.deleteFailedString.current() + .then(str => this.toast.danger(str)); + }, + () => { + this.providerRecord.refreshCurrent().then( + () => this.providerAttributesGrid.reload() + ); + } + ); + }; + this.providerAttributesGrid.onRowActivate.subscribe( + (idlThing: IdlObject) => this.showEditDialog(idlThing) + ); + this.subscription = this.providerRecord.providerUpdated$.subscribe( + id => { + this.providerAttributesGrid.reload(); + } + ); + + } + + ngAfterViewInit() { + console.log('this.providerRecord', this.providerRecord); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + + getDataSource(): GridDataSource { + const gridSource = new GridDataSource(); + + gridSource.getRows = (pager: Pager, sort: any[]) => { + this.provider = this.providerRecord.current(); + if (!this.provider) { + return empty(); + } + let attributes = this.provider.attributes(); + + if (sort.length > 0) { + attributes = attributes.sort((a, b) => { + for (let i = 0; i < sort.length; i++) { + let lt = -1; + const sfield = sort[i].name; + if (sort[i].dir.substring(0, 1).toLowerCase() === 'd') { + lt *= -1; + } + if (a[sfield]() < b[sfield]()) { return lt; } + if (a[sfield]() > b[sfield]()) { return lt * -1; } + } + return 0; + }); + + } + + return from(attributes.slice(pager.offset, pager.offset + pager.limit)); + }; + return gridSource; + } + + showEditDialog(providerAttribute: IdlObject): Promise { + this.editDialog.mode = 'update'; + this.editDialog.recordId = providerAttribute['id'](); + return new Promise((resolve, reject) => { + this.editDialog.open({size: this.dialogSize}).subscribe( + result => { + this.successString.current() + .then(str => this.toast.success(str)); + this.providerRecord.refreshCurrent().then( + () => this.providerAttributesGrid.reload() + ); + resolve(result); + }, + error => { + this.updateFailedString.current() + .then(str => this.toast.danger(str)); + reject(error); + } + ); + }); + } + + editSelected(providerAttributesFields: IdlObject[]) { + // Edit each IDL thing one at a time + const editOneThing = (providerAttributes: IdlObject) => { + if (!providerAttributes) { return; } + + this.showEditDialog(providerAttributes).then( + () => editOneThing(providerAttributesFields.shift())); + }; + + editOneThing(providerAttributesFields.shift()); + } + + createNew() { + this.editDialog.mode = 'create'; + const attributes = this.idl.create('acqlipad'); + attributes.provider(this.provider.id()); + this.editDialog.record = attributes; + this.editDialog.recordId = null; + this.editDialog.open({size: this.dialogSize}).subscribe( + ok => { + this.createString.current() + .then(str => this.toast.success(str)); + this.providerRecord.refreshCurrent().then( + () => this.providerAttributesGrid.reload() + ); + }, + rejection => { + if (!rejection.dismissed) { + this.createErrString.current() + .then(str => this.toast.danger(str)); + } + } + ); + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-contact-addresses.component.html b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-contact-addresses.component.html new file mode 100644 index 0000000000..c2ec30bd76 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-contact-addresses.component.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-contact-addresses.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-contact-addresses.component.ts new file mode 100644 index 0000000000..06d81e21aa --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-contact-addresses.component.ts @@ -0,0 +1,229 @@ +import {Component, OnInit, AfterViewInit, Input, ViewChild} from '@angular/core'; +import {empty, throwError, Observable, from} from 'rxjs'; +import {map} from 'rxjs/operators'; +import {Router, ActivatedRoute, ParamMap} from '@angular/router'; +import {Pager} from '@eg/share/util/pager'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {NetService} from '@eg/core/net.service'; +import {AuthService} from '@eg/core/auth.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {EventService} from '@eg/core/event.service'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid'; +import {ProviderRecord, ProviderRecordService} from './provider-record.service'; +import {AcqProviderSearchFormComponent} from './acq-provider-search-form.component'; +import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ToastService} from '@eg/share/toast/toast.service'; + +@Component({ + selector: 'eg-provider-contact-addresses', + templateUrl: 'provider-contact-addresses.component.html', +}) +export class ProviderContactAddressesComponent implements OnInit, AfterViewInit { + + addresses: any[] = []; + + gridSource: GridDataSource; + @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent; + @ViewChild('acqProviderContactAddressesGrid', { static: true }) providerContactAddressesGrid: GridComponent; + @ViewChild('successString', { static: true }) successString: StringComponent; + @ViewChild('createString', { static: false }) createString: StringComponent; + @ViewChild('createErrString', { static: false }) createErrString: StringComponent; + @ViewChild('updateFailedString', { static: false }) updateFailedString: StringComponent; + @ViewChild('deleteFailedString', { static: true }) deleteFailedString: StringComponent; + @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent; + + cellTextGenerator: GridCellTextGenerator; + + canCreate: boolean; + canDelete: boolean; + deleteSelected: (rows: IdlObject[]) => void; + reloadGrid: () => void; + + permissions: {[name: string]: boolean}; + + // Size of create/edito dialog. Uses large by default. + @Input() dialogSize: 'sm' | 'lg' = 'lg'; + @Input() contactId: any; + + constructor( + private router: Router, + private route: ActivatedRoute, + private net: NetService, + private evt: EventService, + private idl: IdlService, + private auth: AuthService, + private providerRecord: ProviderRecordService, + private toast: ToastService, + private pcrud: PcrudService) { + } + + ngOnInit() { + this.gridSource = this.getDataSource(); + this.cellTextGenerator = {}; + this.reloadGrid = () => this.providerContactAddressesGrid.reload(); + this.deleteSelected = (idlThings: IdlObject[]) => { + idlThings.forEach(idlThing => idlThing.isdeleted(true)); + this.pcrud.autoApply(idlThings).subscribe( + val => { + console.debug('deleted: ' + val); + this.deleteSuccessString.current() + .then(str => this.toast.success(str)); + }, + err => { + this.deleteFailedString.current() + .then(str => this.toast.danger(str)); + }, + () => { + this.providerRecord.refreshCurrent().then( + () => this.providerContactAddressesGrid.reload() + ); + } + ); + }; + this.providerContactAddressesGrid.onRowActivate.subscribe( + (idlThing: IdlObject) => this.showEditDialog(idlThing) + ); + } + + + ngAfterViewInit() { + console.log('this.contactId', this.contactId); + } + + generateSearch(filters): any { + const query: any = new Array(); + + Object.keys(filters).forEach(filterField => { + filters[filterField].forEach(condition => { + query.push(condition); + }); + }); + return query; + } + + getDataSource(): GridDataSource { + const gridSource = new GridDataSource(); + + gridSource.getRows = (pager: Pager, sort: any[]) => { + if (!this.contactId) { + return empty(); + } + const cid = this.contactId; + const contact = this.providerRecord.current().contacts().filter( c => c.id() === cid)[0]; + let addresses = contact.addresses(); + + const query = this.generateSearch(gridSource.filters); + if (query.length) { + query.unshift( { id: addresses.map(a => a.id()) } ); + + const opts = {}; + opts['offset'] = pager.offset; + opts['limit'] = pager.limit; + opts['au_by_id'] = true; + + if (sort.length > 0) { + opts['order_by'] = []; + sort.forEach(sort_clause => { + opts['order_by'].push({ + class: 'acqpca', + field: sort_clause.name, + direction: sort_clause.dir + }); + }); + } + + return this.pcrud.search('acqpca', + query, + opts + ).pipe( + map(res => { + if (this.evt.parse(res)) { + throw throwError(res); + } else { + return res; + } + }), + ); + } + + if (sort.length > 0) { + addresses = addresses.sort((a, b) => { + for (let i = 0; i < sort.length; i++) { + let lt = -1; + const sfield = sort[i].name; + if (sort[i].dir.substring(0, 1).toLowerCase() === 'd') { + lt *= -1; + } + if (a[sfield]() < b[sfield]()) { return lt; } + if (a[sfield]() > b[sfield]()) { return lt * -1; } + } + return 0; + }); + + } + + return from(addresses.slice(pager.offset, pager.offset + pager.limit)); + }; + return gridSource; + } + + showEditDialog(providerContactAddress: IdlObject): Promise { + this.editDialog.mode = 'update'; + this.editDialog.recordId = providerContactAddress['id'](); + return new Promise((resolve, reject) => { + this.editDialog.open({size: this.dialogSize}).subscribe( + result => { + this.successString.current() + .then(str => this.toast.success(str)); + this.providerRecord.refreshCurrent().then( + () => this.providerContactAddressesGrid.reload() + ); + resolve(result); + }, + error => { + this.updateFailedString.current() + .then(str => this.toast.danger(str)); + reject(error); + } + ); + }); + } + + editSelected(providerContactAddressFields: IdlObject[]) { + // Edit each IDL thing one at a time + const editOneThing = (providerContactAddress: IdlObject) => { + if (!providerContactAddress) { return; } + + this.showEditDialog(providerContactAddress).then( + () => editOneThing(providerContactAddressFields.shift())); + }; + + editOneThing(providerContactAddressFields.shift()); + } + + createNew() { + this.editDialog.mode = 'create'; + const address = this.idl.create('acqpca'); + address.contact(this.contactId); + address.valid(true); + this.editDialog.record = address; + this.editDialog.recordId = null; + this.editDialog.open({size: this.dialogSize}).subscribe( + ok => { + this.createString.current() + .then(str => this.toast.success(str)); + this.providerRecord.refreshCurrent().then( + () => this.providerContactAddressesGrid.reload() + ); + }, + rejection => { + if (!rejection.dismissed) { + this.createErrString.current() + .then(str => this.toast.danger(str)); + } + } + ); + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-contacts.component.html b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-contacts.component.html new file mode 100644 index 0000000000..694ca44413 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-contacts.component.html @@ -0,0 +1,68 @@ + + + + + + + + + + + + + {{contact.email()}} + + + + {{contact.phone()}} + + + + + + + + + + + + + + + + + + + + + + + + + +

Addresses for: {{selectedContact.name()}}

+ + +
+ + + + diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-contacts.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-contacts.component.ts new file mode 100644 index 0000000000..ddef6e044b --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-contacts.component.ts @@ -0,0 +1,325 @@ +import {Component, OnInit, AfterViewInit, OnDestroy, Input, Output, ViewChild, EventEmitter, ChangeDetectorRef} from '@angular/core'; +import {empty, throwError, Observable, from, Subscription} from 'rxjs'; +import {map} from 'rxjs/operators'; +import {Router, ActivatedRoute, ParamMap} from '@angular/router'; +import {Pager} from '@eg/share/util/pager'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {NetService} from '@eg/core/net.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {EventService} from '@eg/core/event.service'; +import {AuthService} from '@eg/core/auth.service'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid'; +import {ProviderRecordService} from './provider-record.service'; +import {ProviderContactAddressesComponent} from './provider-contact-addresses.component'; +import {AcqProviderSearchFormComponent} from './acq-provider-search-form.component'; +import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component'; +import {ToastService} from '@eg/share/toast/toast.service'; + + +@Component({ + selector: 'eg-provider-contacts', + templateUrl: 'provider-contacts.component.html', +}) +export class ProviderContactsComponent implements OnInit, AfterViewInit, OnDestroy { + + @Input() providerId: any; + contacts: any[] = []; + + gridSource: GridDataSource; + @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent; + @ViewChild('providerContactAddresses', { static: false }) providerContactAddresses: ProviderContactAddressesComponent; + @ViewChild('acqProviderContactsGrid', { static: true }) providerContactsGrid: GridComponent; + @ViewChild('confirmSetAsPrimary', { static: true }) confirmSetAsPrimary: ConfirmDialogComponent; + @ViewChild('confirmUnsetAsPrimary', { static: true }) confirmUnsetAsPrimary: ConfirmDialogComponent; + @ViewChild('successString', { static: true }) successString: StringComponent; + @ViewChild('createString', { static: false }) createString: StringComponent; + @ViewChild('createErrString', { static: false }) createErrString: StringComponent; + @ViewChild('updateFailedString', { static: false }) updateFailedString: StringComponent; + @ViewChild('deleteFailedString', { static: true }) deleteFailedString: StringComponent; + @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent; + @ViewChild('setAsPrimarySuccessString', { static: true }) setAsPrimarySuccessString: StringComponent; + @ViewChild('setAsPrimaryFailedString', { static: true }) setAsPrimaryFailedString: StringComponent; + @ViewChild('unsetAsPrimarySuccessString', { static: true }) unsetAsPrimarySuccessString: StringComponent; + @ViewChild('unsetAsPrimaryFailedString', { static: true }) unsetAsPrimaryFailedString: StringComponent; + + @Output() desireSummarize: EventEmitter = new EventEmitter(); + + cellTextGenerator: GridCellTextGenerator; + provider: IdlObject; + selectedContact: IdlObject; + + canCreate: boolean; + canDelete: boolean; + deleteSelected: (rows: IdlObject[]) => void; + cannotSetPrimaryContact: (rows: IdlObject[]) => boolean; + cannotUnsetPrimaryContact: (rows: IdlObject[]) => boolean; + + permissions: {[name: string]: boolean}; + + subscription: Subscription; + + // Size of create/edito dialog. Uses large by default. + @Input() dialogSize: 'sm' | 'lg' = 'lg'; + + constructor( + private router: Router, + private route: ActivatedRoute, + private changeDetector: ChangeDetectorRef, + private net: NetService, + private pcrud: PcrudService, + private evt: EventService, + private auth: AuthService, + private idl: IdlService, + private providerRecord: ProviderRecordService, + private toast: ToastService) { + } + + ngOnInit() { + this.gridSource = this.getDataSource(); + this.cellTextGenerator = {}; + this.cannotSetPrimaryContact = (rows: IdlObject[]) => (rows.length !== 1 || (rows.length === 1 && rows[0]._is_primary)); + this.cannotUnsetPrimaryContact = (rows: IdlObject[]) => (rows.length !== 1 || (rows.length === 1 && !rows[0]._is_primary)); + this.deleteSelected = (idlThings: IdlObject[]) => { + idlThings.forEach(idlThing => idlThing.isdeleted(true)); + this.providerRecord.batchUpdate(idlThings).subscribe( + val => { + console.debug('deleted: ' + val); + this.deleteSuccessString.current() + .then(str => this.toast.success(str)); + this.desireSummarize.emit(this.provider.id()); + }, + err => { + this.deleteFailedString.current() + .then(str => this.toast.danger(str)); + }, + () => { + this.providerRecord.refreshCurrent().then( + () => this.providerContactsGrid.reload() + ); + } + ); + }; + this.providerContactsGrid.onRowActivate.subscribe( + (idlThing: IdlObject) => this.showEditDialog(idlThing) + ); + this.subscription = this.providerRecord.providerUpdated$.subscribe( + id => { + this.providerContactsGrid.reload(); + } + ); + } + + ngAfterViewInit() { + console.log('this.providerRecord', this.providerRecord); + console.log('this.providerContactAddresses', this.providerContactAddresses); + this.providerContactsGrid.onRowClick.subscribe( + (idlThing: IdlObject) => { + this.selectedContact = idlThing; + console.debug('selected contact', this.selectedContact); + // ensure that the contact address grid is instantiated + this.changeDetector.detectChanges(); + this.providerContactAddresses.reloadGrid(); + } + ); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + + generateSearch(filters): any { + const query: any = new Array(); + + Object.keys(filters).forEach(filterField => { + filters[filterField].forEach(condition => { + query.push(condition); + }); + }); + return query; + } + + getDataSource(): GridDataSource { + const gridSource = new GridDataSource(); + + gridSource.getRows = (pager: Pager, sort: any[]) => { + this.provider = this.providerRecord.current(); + if (!this.provider) { + return empty(); + } + let contacts = this.provider.contacts(); + + const query = this.generateSearch(gridSource.filters); + if (query.length) { + query.unshift( { id: contacts.map(a => a.id()) } ); + + const opts = {}; + opts['offset'] = pager.offset; + opts['limit'] = pager.limit; + opts['au_by_id'] = true; + + if (sort.length > 0) { + opts['order_by'] = []; + sort.forEach(sort_clause => { + opts['order_by'].push({ + class: 'acqpc', + field: sort_clause.name, + direction: sort_clause.dir + }); + }); + } + + return this.pcrud.search('acqpc', + query, + opts + ).pipe( + map(res => { + if (this.evt.parse(res)) { + throw throwError(res); + } else { + return res; + } + }), + ); + } + + if (sort.length > 0) { + contacts = contacts.sort((a, b) => { + for (let i = 0; i < sort.length; i++) { + let lt = -1; + const sfield = sort[i].name; + if (sort[i].dir.substring(0, 1).toLowerCase() === 'd') { + lt *= -1; + } + if (a[sfield]() < b[sfield]()) { return lt; } + if (a[sfield]() > b[sfield]()) { return lt * -1; } + } + return 0; + }); + + } + + return from(contacts.slice(pager.offset, pager.offset + pager.limit)); + }; + return gridSource; + } + + showEditDialog(providerContact: IdlObject): Promise { + this.editDialog.mode = 'update'; + this.editDialog.recordId = providerContact['id'](); + return new Promise((resolve, reject) => { + this.editDialog.open({size: this.dialogSize}).subscribe( + result => { + this.successString.current() + .then(str => this.toast.success(str)); + this.providerRecord.refreshCurrent().then( + () => this.providerContactsGrid.reload() + ); + this.desireSummarize.emit(this.provider.id()); + resolve(result); + }, + error => { + this.updateFailedString.current() + .then(str => this.toast.danger(str)); + reject(error); + } + ); + }); + } + + editSelected(providerContactFields: IdlObject[]) { + // Edit each IDL thing one at a time + const editOneThing = (providerContact: IdlObject) => { + if (!providerContact) { return; } + + this.showEditDialog(providerContact).then( + () => editOneThing(providerContactFields.shift())); + }; + + editOneThing(providerContactFields.shift()); + } + + createNew() { + this.editDialog.mode = 'create'; + const contact = this.idl.create('acqpc'); + contact.provider(this.provider.id()); + this.editDialog.record = contact; + this.editDialog.recordId = null; + this.editDialog.open({size: this.dialogSize}).subscribe( + ok => { + this.createString.current() + .then(str => this.toast.success(str)); + this.providerRecord.refreshCurrent().then( + () => this.providerContactsGrid.reload() + ); + this.desireSummarize.emit(this.provider.id()); + }, + rejection => { + if (!rejection.dismissed) { + this.createErrString.current() + .then(str => this.toast.danger(str)); + } + } + ); + } + + setAsPrimary(providerContacts: IdlObject[]) { + this.selectedContact = providerContacts[0]; + this.confirmSetAsPrimary.open().subscribe(confirmed => { + if (!confirmed) { return; } + this.providerRecord.refreshCurrent().then(() => { + this.provider.primary_contact(providerContacts[0].id()); + this.provider.ischanged(true); + this.providerRecord.batchUpdate(this.provider).subscribe( + val => { + this.setAsPrimarySuccessString.current() + .then(str => this.toast.success(str)); + }, + err => { + this.setAsPrimaryFailedString.current() + .then(str => this.toast.danger(str)); + }, + () => { + this.providerRecord.refreshCurrent().then( + () => { + this.providerContactsGrid.reload(); + this.desireSummarize.emit(this.provider.id()); + } + ); + } + ); + }); + }); + } + + unsetAsPrimary(providerContacts: IdlObject[]) { + this.selectedContact = providerContacts[0]; + this.confirmUnsetAsPrimary.open().subscribe(confirmed => { + if (!confirmed) { return; } + this.providerRecord.refreshCurrent().then(() => { + this.provider.primary_contact(null); + this.provider.ischanged(true); + this.providerRecord.batchUpdate(this.provider).subscribe( + val => { + this.unsetAsPrimarySuccessString.current() + .then(str => this.toast.success(str)); + }, + err => { + this.unsetAsPrimaryFailedString.current() + .then(str => this.toast.danger(str)); + }, + () => { + this.providerRecord.refreshCurrent().then( + () => { + this.providerContactsGrid.reload(); + this.desireSummarize.emit(this.provider.id()); + } + ); + } + ); + }); + }); + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-details.component.html b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-details.component.html new file mode 100644 index 0000000000..189e57cb56 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-details.component.html @@ -0,0 +1,17 @@ + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-details.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-details.component.ts new file mode 100644 index 0000000000..4e537265a7 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-details.component.ts @@ -0,0 +1,82 @@ +import {Component, OnInit, Output, EventEmitter, ViewChild} from '@angular/core'; +import {empty, throwError, Observable, from} from 'rxjs'; +import {map} from 'rxjs/operators'; +import {Router, ActivatedRoute, ParamMap} from '@angular/router'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {NetService} from '@eg/core/net.service'; +import {AuthService} from '@eg/core/auth.service'; +import {ProviderRecord, ProviderRecordService} from './provider-record.service'; +import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ToastService} from '@eg/share/toast/toast.service'; + +@Component({ + selector: 'eg-provider-details', + templateUrl: 'provider-details.component.html', +}) +export class ProviderDetailsComponent implements OnInit { + + @ViewChild('successString', { static: true }) successString: StringComponent; + @ViewChild('updateFailedString', { static: false }) updateFailedString: StringComponent; + @ViewChild('deleteFailedString', { static: true }) deleteFailedString: StringComponent; + @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent; + @ViewChild('editDialog', { static: false}) editDialog: FmRecordEditorComponent; + + provider: IdlObject; + + permissions: {[name: string]: boolean}; + + @Output() desireSummarize: EventEmitter = new EventEmitter(); + + constructor( + private router: Router, + private route: ActivatedRoute, + private net: NetService, + private idl: IdlService, + private auth: AuthService, + private providerRecord: ProviderRecordService, + private toast: ToastService) { + } + + ngOnInit() { + this.refresh(); + } + + _deflesh() { + if (!this.provider) { + return; + } + // unflesh the currency type and edi_default fields so that the + // record editor can display them + // TODO: make fm-editor be able to handle fleshed linked fields + if (this.provider.currency_type()) { + this.provider.currency_type(this.provider.currency_type().code()); + } + if (this.provider.edi_default() && typeof this.provider.edi_default() !== 'number') { + this.provider.edi_default(this.provider.edi_default().id()); + } + } + + updateProvider(providerId: any) { + this.desireSummarize.emit(this.provider.id()); + } + + refresh() { + this.provider = this.idl.clone(this.providerRecord.current()); + this._deflesh(); + } + + permittedMode(): string { + // TODO - looks like fm-editor may have (via its modePerms) incompletely-implemented + // work to vary the mode depending on whether the user has permission + // to update a record, which would make this moot. + if (!this.providerRecord.currentProviderRecord()) { + return 'view'; + } + return this.providerRecord.currentProviderRecord().canAdmin ? 'update' : 'view'; + } + + isDirty(): boolean { + return (this.editDialog) ? this.editDialog.isDirty() : false; + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-edi-accounts.component.html b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-edi-accounts.component.html new file mode 100644 index 0000000000..af7bb1ddeb --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-edi-accounts.component.html @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

EDI messages for account {{selectedEdiAccountLabel}}

+ + +
+ + + + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-edi-accounts.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-edi-accounts.component.ts new file mode 100644 index 0000000000..9ecf4f2db3 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-edi-accounts.component.ts @@ -0,0 +1,281 @@ +import {Component, OnInit, AfterViewInit, OnDestroy, Input, Output, EventEmitter, ViewChild, ChangeDetectorRef} from '@angular/core'; +import {empty, throwError, Observable, from, Subscription} from 'rxjs'; +import {map} from 'rxjs/operators'; +import {Router, ActivatedRoute, ParamMap} from '@angular/router'; +import {Pager} from '@eg/share/util/pager'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {NetService} from '@eg/core/net.service'; +import {AuthService} from '@eg/core/auth.service'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid'; +import {ProviderRecord, ProviderRecordService} from './provider-record.service'; +import {AcqProviderSearchFormComponent} from './acq-provider-search-form.component'; +import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component'; +import {PcrudService} from '@eg/core/pcrud.service'; + +@Component({ + selector: 'eg-provider-edi-accounts', + templateUrl: 'provider-edi-accounts.component.html', +}) +export class ProviderEdiAccountsComponent implements OnInit, AfterViewInit, OnDestroy { + + edi_accounts: any[] = []; + + gridSource: GridDataSource; + ediMessagesSource: GridDataSource; + @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent; + @ViewChild('acqProviderEdiAccountsGrid', { static: true }) providerEdiAccountsGrid: GridComponent; + @ViewChild('acqProviderEdiMessagesGrid', { static: false }) providerEdiMessagesGrid: GridComponent; + @ViewChild('confirmSetAsDefault', { static: true }) confirmSetAsDefault: ConfirmDialogComponent; + @ViewChild('successString', { static: true }) successString: StringComponent; + @ViewChild('createString', { static: false }) createString: StringComponent; + @ViewChild('createErrString', { static: false }) createErrString: StringComponent; + @ViewChild('updateFailedString', { static: false }) updateFailedString: StringComponent; + @ViewChild('deleteFailedString', { static: true }) deleteFailedString: StringComponent; + @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent; + @ViewChild('setAsDefaultSuccessString', { static: true }) setAsDefaultSuccessString: StringComponent; + @ViewChild('setAsDefaultFailedString', { static: true }) setAsDefaultFailedString: StringComponent; + + cellTextGenerator: GridCellTextGenerator; + provider: IdlObject; + selected: IdlObject; + + canCreate: boolean; + canDelete: boolean; + notOneSelectedRow: (rows: IdlObject[]) => boolean; + deleteSelected: (rows: IdlObject[]) => void; + + viewEdiMessages: boolean; + selectedEdiAccountId: number; + selectedEdiAccountLabel = ''; + + permissions: {[name: string]: boolean}; + + subscription: Subscription; + + // Size of create/edito dialog. Uses large by default. + @Input() dialogSize: 'sm' | 'lg' = 'lg'; + @Output() desireSummarize: EventEmitter = new EventEmitter(); + + constructor( + private router: Router, + private route: ActivatedRoute, + private changeDetector: ChangeDetectorRef, + private net: NetService, + private idl: IdlService, + private auth: AuthService, + private pcrud: PcrudService, + private providerRecord: ProviderRecordService, + private toast: ToastService) { + } + + ngOnInit() { + this.gridSource = this.getDataSource(); + this.ediMessagesSource = this.getEdiMessagesSource(); + this.viewEdiMessages = false; + this.selectedEdiAccountId = null; + this.cellTextGenerator = {}; + this.notOneSelectedRow = (rows: IdlObject[]) => (rows.length !== 1); + this.deleteSelected = (idlThings: IdlObject[]) => { + idlThings.forEach(idlThing => idlThing.isdeleted(true)); + this.providerRecord.batchUpdate(idlThings).subscribe( + val => { + console.debug('deleted: ' + val); + this.deleteSuccessString.current() + .then(str => this.toast.success(str)); + }, + err => { + this.deleteFailedString.current() + .then(str => this.toast.danger(str)); + }, + () => { + this.providerRecord.refreshCurrent().then( + () => { + this.providerEdiAccountsGrid.reload(); + } + ); + } + ); + }; + this.providerEdiAccountsGrid.onRowActivate.subscribe( + (idlThing: IdlObject) => this.showEditDialog(idlThing) + ); + this.subscription = this.providerRecord.providerUpdated$.subscribe( + id => { + this.providerEdiAccountsGrid.reload(); + } + ); + } + + ngAfterViewInit() { + console.log('this.providerRecord', this.providerRecord); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + + getDataSource(): GridDataSource { + const gridSource = new GridDataSource(); + + gridSource.getRows = (pager: Pager, sort: any[]) => { + this.provider = this.providerRecord.current(); + if (!this.provider) { + return empty(); + } + let edi_accounts = this.provider.edi_accounts(); + + if (sort.length > 0) { + edi_accounts = edi_accounts.sort((a, b) => { + for (let i = 0; i < sort.length; i++) { + let lt = -1; + const sfield = sort[i].name; + if (sort[i].dir.substring(0, 1).toLowerCase() === 'd') { + lt *= -1; + } + if (a[sfield]() < b[sfield]()) { return lt; } + if (a[sfield]() > b[sfield]()) { return lt * -1; } + } + return 0; + }); + + } + + return from(edi_accounts.slice(pager.offset, pager.offset + pager.limit)); + }; + return gridSource; + } + + getEdiMessagesSource(): GridDataSource { + const gridSource = new GridDataSource(); + gridSource.getRows = (pager: Pager, sort: any[]) => { + const orderBy: any = {acqedim: 'create_time desc'}; + if (sort.length) { + orderBy.acqedim = sort[0].name + ' ' + sort[0].dir; + } + + // base query to grab everything + const base: Object = { + account: this.selectedEdiAccountId + }; + const query: any = new Array(); + query.push(base); + + // and add any filters + Object.keys(gridSource.filters).forEach(key => { + Object.keys(gridSource.filters[key]).forEach(key2 => { + query.push(gridSource.filters[key][key2]); + }); + }); + return this.pcrud.search('acqedim', + query, { + flesh: 3, + flesh_fields: {acqedim: ['account', 'purchase_order']}, + offset: pager.offset, + limit: pager.limit, + order_by: orderBy + }); + }; + return gridSource; + } + + showEditDialog(providerEdiAccount: IdlObject): Promise { + this.editDialog.mode = 'update'; + this.editDialog.recordId = providerEdiAccount['id'](); + return new Promise((resolve, reject) => { + this.editDialog.open({size: this.dialogSize}).subscribe( + result => { + this.successString.current() + .then(str => this.toast.success(str)); + this.providerRecord.refreshCurrent().then( + () => this.providerEdiAccountsGrid.reload() + ); + resolve(result); + }, + error => { + this.updateFailedString.current() + .then(str => this.toast.danger(str)); + reject(error); + } + ); + }); + } + + editSelected(providerEdiAccountFields: IdlObject[]) { + // Edit each IDL thing one at a time + const editOneThing = (providerEdiAccount: IdlObject) => { + if (!providerEdiAccount) { return; } + + this.showEditDialog(providerEdiAccount).then( + () => editOneThing(providerEdiAccountFields.shift())); + }; + + editOneThing(providerEdiAccountFields.shift()); + } + + setAsDefault(providerEdiAccountFields: IdlObject[]) { + this.selected = providerEdiAccountFields[0]; + this.confirmSetAsDefault.open().subscribe(confirmed => { + if (!confirmed) { return; } + this.providerRecord.refreshCurrent().then(() => { + this.provider.edi_default(providerEdiAccountFields[0].id()); + this.provider.ischanged(true); + this.providerRecord.batchUpdate(this.provider).subscribe( + val => { + this.setAsDefaultSuccessString.current() + .then(str => this.toast.success(str)); + }, + err => { + this.setAsDefaultFailedString.current() + .then(str => this.toast.danger(str)); + }, + () => { + this.providerRecord.refreshCurrent().then( + () => { + this.providerEdiAccountsGrid.reload(); + this.desireSummarize.emit(this.provider.id()); + } + ); + } + ); + }); + }); + } + + displayEdiMessages(providerEdiAccountFields: IdlObject[]) { + this.selectedEdiAccountId = providerEdiAccountFields[0].id(); + this.selectedEdiAccountLabel = providerEdiAccountFields[0].label(); + this.viewEdiMessages = true; + this.changeDetector.detectChanges(); + this.providerEdiMessagesGrid.reload(); + } + + createNew() { + this.editDialog.mode = 'create'; + const edi_account = this.idl.create('acqedi'); + edi_account.provider(this.provider.id()); + edi_account.owner(this.auth.user().ws_ou()); + edi_account.use_attrs(true); + this.editDialog.record = edi_account; + this.editDialog.recordId = null; + this.editDialog.open({size: this.dialogSize}).subscribe( + ok => { + this.createString.current() + .then(str => this.toast.success(str)); + this.providerRecord.refreshCurrent().then( + () => this.providerEdiAccountsGrid.reload() + ); + }, + rejection => { + if (!rejection.dismissed) { + this.createErrString.current() + .then(str => this.toast.danger(str)); + } + } + ); + } + +} diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-holdings.component.html b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-holdings.component.html new file mode 100644 index 0000000000..897ddf8c91 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-holdings.component.html @@ -0,0 +1,83 @@ + + + + + + + + + + + + Barcode + Call Number + Circulation Modifier + Collection Code + Estimated Price + Fund Code + Note + Owning Library + Quantity + Shelving Location + {{row.name()}} + + + +
+
+ +
+ +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-holdings.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-holdings.component.ts new file mode 100644 index 0000000000..7b2ae2fb48 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-holdings.component.ts @@ -0,0 +1,223 @@ +import {Component, OnInit, AfterViewInit, OnDestroy, Input, ViewChild} from '@angular/core'; +import {NgForm} from '@angular/forms'; +import {empty, throwError, Observable, from, Subscription} from 'rxjs'; +import {map} from 'rxjs/operators'; +import {Router, ActivatedRoute, ParamMap} from '@angular/router'; +import {Pager} from '@eg/share/util/pager'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {NetService} from '@eg/core/net.service'; +import {AuthService} from '@eg/core/auth.service'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid'; +import {ProviderRecordService} from './provider-record.service'; +import {AcqProviderSearchFormComponent} from './acq-provider-search-form.component'; +import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ToastService} from '@eg/share/toast/toast.service'; + + +@Component({ + selector: 'eg-provider-holdings', + templateUrl: 'provider-holdings.component.html', +}) +export class ProviderHoldingsComponent implements OnInit, AfterViewInit, OnDestroy { + + @Input() providerId: any; + holdings: any[] = []; + + gridSource: GridDataSource; + @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent; + @ViewChild('acqProviderHoldingsGrid', { static: true }) providerHoldingsGrid: GridComponent; + @ViewChild('successString', { static: true }) successString: StringComponent; + @ViewChild('createString', { static: false }) createString: StringComponent; + @ViewChild('createErrString', { static: false }) createErrString: StringComponent; + @ViewChild('updateFailedString', { static: false }) updateFailedString: StringComponent; + @ViewChild('deleteFailedString', { static: true }) deleteFailedString: StringComponent; + @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent; + @ViewChild('successTagString', { static: true }) successTagString: StringComponent; + @ViewChild('updateFailedTagString', { static: false }) updateFailedTagString: StringComponent; + @ViewChild('holdingTagForm', { static: false}) holdingTagForm: NgForm; + + cellTextGenerator: GridCellTextGenerator; + provider: IdlObject; + + canCreate: boolean; + canDelete: boolean; + deleteSelected: (rows: IdlObject[]) => void; + + permissions: {[name: string]: boolean}; + + subscription: Subscription; + + // Size of create/edito dialog. Uses large by default. + @Input() dialogSize: 'sm' | 'lg' = 'lg'; + + constructor( + private router: Router, + private route: ActivatedRoute, + private net: NetService, + private auth: AuthService, + private idl: IdlService, + private providerRecord: ProviderRecordService, + private toast: ToastService) { + + } + + ngOnInit() { + this.gridSource = this.getDataSource(); + this.cellTextGenerator = {}; + this.deleteSelected = (idlThings: IdlObject[]) => { + idlThings.forEach(idlThing => idlThing.isdeleted(true)); + this.providerRecord.batchUpdate(idlThings).subscribe( + val => { + console.debug('deleted: ' + val); + this.deleteSuccessString.current() + .then(str => this.toast.success(str)); + }, + err => { + this.deleteFailedString.current() + .then(str => this.toast.danger(str)); + }, + () => { + this.providerRecord.refreshCurrent().then( + () => this.providerHoldingsGrid.reload() + ); + } + ); + }; + this.providerHoldingsGrid.onRowActivate.subscribe( + (idlThing: IdlObject) => this.showEditDialog(idlThing) + ); + this.subscription = this.providerRecord.providerUpdated$.subscribe( + id => { + this.providerHoldingsGrid.reload(); + } + ); + } + + ngAfterViewInit() { + if (this.providerRecord.current()) { + // sometimes needs to force a refresh in case we updated that tag, + // navigated away (and confirmed that we wanted to abandon the change), + // then navigated back + this.providerRecord.current()['_holding_tag'] = this.providerRecord.current().holding_tag(); + } + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + + updateProvider(providerId: any) { + this.provider.holding_tag(this.provider._holding_tag); + this.provider.ischanged(true); + this.providerRecord.batchUpdate([this.provider]).subscribe( + val => { + this.successTagString.current() + .then(str => this.toast.success(str)); + }, + err => { + this.updateFailedTagString.current() + .then(str => this.toast.danger(str)); + }, + () => { + this.providerRecord.refreshCurrent().then( + () => { this.provider = this.providerRecord.current(); } + ); + } + ); + } + + getDataSource(): GridDataSource { + const gridSource = new GridDataSource(); + + gridSource.getRows = (pager: Pager, sort: any[]) => { + this.provider = this.providerRecord.current(); + if (!this.provider) { + return empty(); + } + let holdings = this.provider.holdings_subfields(); + + if (sort.length > 0) { + holdings = holdings.sort((a, b) => { + for (let i = 0; i < sort.length; i++) { + let lt = -1; + const sfield = sort[i].name; + if (sort[i].dir.substring(0, 1).toLowerCase() === 'd') { + lt *= -1; + } + if (a[sfield]() < b[sfield]()) { return lt; } + if (a[sfield]() > b[sfield]()) { return lt * -1; } + } + return 0; + }); + + } + + return from(holdings.slice(pager.offset, pager.offset + pager.limit)); + }; + return gridSource; + } + + showEditDialog(providerHolding: IdlObject): Promise { + this.editDialog.mode = 'update'; + this.editDialog.recordId = providerHolding['id'](); + return new Promise((resolve, reject) => { + this.editDialog.open({size: this.dialogSize}).subscribe( + result => { + this.successString.current() + .then(str => this.toast.success(str)); + this.providerRecord.refreshCurrent().then( + () => this.providerHoldingsGrid.reload() + ); + resolve(result); + }, + error => { + this.updateFailedString.current() + .then(str => this.toast.danger(str)); + reject(error); + } + ); + }); + } + + editSelected(providerHoldingsFields: IdlObject[]) { + // Edit each IDL thing one at a time + const editOneThing = (providerHoldings: IdlObject) => { + if (!providerHoldings) { return; } + + this.showEditDialog(providerHoldings).then( + () => editOneThing(providerHoldingsFields.shift())); + }; + + editOneThing(providerHoldingsFields.shift()); + } + + createNew() { + this.editDialog.mode = 'create'; + const holdings = this.idl.create('acqphsm'); + holdings.provider(this.provider.id()); + this.editDialog.record = holdings; + this.editDialog.recordId = null; + this.editDialog.open({size: this.dialogSize}).subscribe( + ok => { + this.createString.current() + .then(str => this.toast.success(str)); + this.providerRecord.refreshCurrent().then( + () => this.providerHoldingsGrid.reload() + ); + }, + rejection => { + if (!rejection.dismissed) { + this.createErrString.current() + .then(str => this.toast.danger(str)); + } + } + ); + } + + isDirty(): boolean { + return (this.providerRecord.current()['_holding_tag'] === this.providerRecord.current().holding_tag()) ? false : + (this.holdingTagForm && this.holdingTagForm.dirty) ? this.holdingTagForm.dirty : false; + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-invoices.component.html b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-invoices.component.html new file mode 100644 index 0000000000..feda594d6b --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-invoices.component.html @@ -0,0 +1,45 @@ + + + {{invoice.inv_ident()}} + + + + + {{invoice.provider().code()}} + + + + + {{invoice.shipper().code()}} + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-invoices.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-invoices.component.ts new file mode 100644 index 0000000000..c6a85e01f9 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-invoices.component.ts @@ -0,0 +1,120 @@ +import {Component, OnInit, AfterViewInit, OnDestroy, Input, ViewChild} from '@angular/core'; +import {Observable, Subscription} from 'rxjs'; +import {map} from 'rxjs/operators'; +import {Router, ActivatedRoute, ParamMap} from '@angular/router'; +import {Pager} from '@eg/share/util/pager'; +import {IdlObject} from '@eg/core/idl.service'; +import {EventService} from '@eg/core/event.service'; +import {AlertDialogComponent} from '@eg/share/dialog/alert.component'; +import {PrintService} from '@eg/share/print/print.service'; +import {NetService} from '@eg/core/net.service'; +import {AuthService} from '@eg/core/auth.service'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid'; +import {AcqSearchService, AcqSearchTerm, AcqSearch} from '../search/acq-search.service'; +import {AttrDefsService} from '../search/attr-defs.service'; +import {ProviderRecord, ProviderRecordService} from './provider-record.service'; + +@Component({ + selector: 'eg-provider-invoices', + templateUrl: 'provider-invoices.component.html', + providers: [AcqSearchService, AttrDefsService] +}) +export class ProviderInvoicesComponent implements OnInit, AfterViewInit, OnDestroy { + + @Input() initialSearchTerms: AcqSearchTerm[] = []; + + gridSource: GridDataSource; + @ViewChild('acqProviderInvoicesGrid', { static: true }) providerInvoicesGrid: GridComponent; + @ViewChild('printfail', { static: true }) private printfail: AlertDialogComponent; + + noSelectedRows: (rows: IdlObject[]) => boolean; + + cellTextGenerator: GridCellTextGenerator; + + subscription: Subscription; + + constructor( + private router: Router, + private route: ActivatedRoute, + private printer: PrintService, + private evt: EventService, + private net: NetService, + private auth: AuthService, + private providerRecord: ProviderRecordService, + private acqSearch: AcqSearchService) { + } + + ngOnInit() { + this.gridSource = this.acqSearch.getAcqSearchDataSource('invoice'); + this.noSelectedRows = (rows: IdlObject[]) => (rows.length === 0); + this.cellTextGenerator = { + inv_ident: row => row.inv_ident(), + provider: row => row.provider().code(), + shipper: row => row.shipper().code(), + }; + this.subscription = this.providerRecord.providerUpdated$.subscribe( + id => { + this.resetSearch(); + } + ); + } + + ngAfterViewInit() { + this.resetSearch(); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + + resetSearch() { + const provider = this.providerRecord.current(); + if (provider) { + setTimeout(() => { + this.acqSearch.setSearch({ + terms: [{ + field: 'acqinv:provider', + op: '', + value1: provider.id(), + value2: '', + }], + conjunction: 'all', + }); + this.providerInvoicesGrid.reload(); + }); + } + } + + // TODO - copied from InvoiceResultsComponent, could be + // consolidated + printSelectedInvoices(rows: IdlObject[]) { + const that = this; + let html = '\n'; + this.net.request( + 'open-ils.acq', + 'open-ils.acq.invoice.print.html', + this.auth.token(), rows.map( invoice => invoice.id() ) + ).subscribe( + (res) => { + if (this.evt.parse(res)) { + console.error(res); + this.printfail.open(); + } else { + html += res.template_output().data(); + } + }, + (err) => { + console.error(err); + this.printfail.open(); + }, + () => this.printer.print({ + text: html, + printContext: 'default' + }) + ); + } + +} diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-purchase-orders.component.html b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-purchase-orders.component.html new file mode 100644 index 0000000000..caed7ae8f1 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-purchase-orders.component.html @@ -0,0 +1,38 @@ + + + {{purchaseorder.name()}} + + + + + + {{purchaseorder.provider().code()}} + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-purchase-orders.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-purchase-orders.component.ts new file mode 100644 index 0000000000..521a31f028 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-purchase-orders.component.ts @@ -0,0 +1,89 @@ +import {Component, OnInit, AfterViewInit, OnDestroy, Input, ViewChild} from '@angular/core'; +import {Observable, Subscription} from 'rxjs'; +import {map} from 'rxjs/operators'; +import {Router, ActivatedRoute, ParamMap} from '@angular/router'; +import {Pager} from '@eg/share/util/pager'; +import {IdlObject} from '@eg/core/idl.service'; +import {EventService} from '@eg/core/event.service'; +import {AlertDialogComponent} from '@eg/share/dialog/alert.component'; +import {PrintService} from '@eg/share/print/print.service'; +import {NetService} from '@eg/core/net.service'; +import {AuthService} from '@eg/core/auth.service'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid'; +import {AcqSearchService, AcqSearchTerm, AcqSearch} from '../search/acq-search.service'; +import {AttrDefsService} from '../search/attr-defs.service'; +import {ProviderRecord, ProviderRecordService} from './provider-record.service'; + +@Component({ + selector: 'eg-provider-purchase-orders', + templateUrl: 'provider-purchase-orders.component.html', + providers: [AcqSearchService, AttrDefsService] +}) +export class ProviderPurchaseOrdersComponent implements OnInit, AfterViewInit, OnDestroy { + + @Input() initialSearchTerms: AcqSearchTerm[] = []; + + gridSource: GridDataSource; + @ViewChild('acqProviderPurchaseOrdersGrid', { static: true }) providerPurchaseOrdersGrid: GridComponent; + @ViewChild('printfail', { static: true }) private printfail: AlertDialogComponent; + + noSelectedRows: (rows: IdlObject[]) => boolean; + + cellTextGenerator: GridCellTextGenerator; + + subscription: Subscription; + + constructor( + private router: Router, + private route: ActivatedRoute, + private printer: PrintService, + private evt: EventService, + private net: NetService, + private auth: AuthService, + private providerRecord: ProviderRecordService, + private acqSearch: AcqSearchService) { + } + + ngOnInit() { + this.gridSource = this.acqSearch.getAcqSearchDataSource('purchase_order'); + this.noSelectedRows = (rows: IdlObject[]) => (rows.length === 0); + this.cellTextGenerator = { + inv_ident: row => row.inv_ident(), + provider: row => row.provider().code(), + shipper: row => row.shipper().code(), + }; + this.subscription = this.providerRecord.providerUpdated$.subscribe( + id => { + this.resetSearch(); + } + ); + } + + ngAfterViewInit() { + this.resetSearch(); + } + + resetSearch() { + const provider = this.providerRecord.current(); + if (provider) { + setTimeout(() => { + this.acqSearch.setSearch({ + terms: [{ + field: 'acqpo:provider', + op: '', + value1: provider.id(), + value2: '', + }], + conjunction: 'all', + }); + this.providerPurchaseOrdersGrid.reload(); + }); + } + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + +} diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-record.service.ts b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-record.service.ts new file mode 100644 index 0000000000..4f46b512c9 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-record.service.ts @@ -0,0 +1,198 @@ +import {Injectable} from '@angular/core'; +import {Observable, from} from 'rxjs'; +import {empty, throwError, Subject} from 'rxjs'; +import {map, defaultIfEmpty} from 'rxjs/operators'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {NetService} from '@eg/core/net.service'; +import {PermService} from '@eg/core/perm.service'; + +export class ProviderSummary { +} + +export class ProviderRecord { + id: number; + record: IdlObject; + canDelete: boolean; + canAdmin: boolean; + + constructor(record: IdlObject) { + this.id = Number(record.id()); + this.record = record; + this.canDelete = false; + this.canAdmin = false; + } +} + +@Injectable() +export class ProviderRecordService { + + private currentProvider: ProviderRecord; + private currentProviderId: number = null; + + private providerUpdatedSource = new Subject(); + providerUpdated$ = this.providerUpdatedSource.asObservable(); + + private permissions: any; + private viewOUs: number[] = []; + + constructor( + private idl: IdlService, + private net: NetService, + private pcrud: PcrudService, + private perm: PermService + ) { + this.currentProvider = null; + this.loadPerms(); + } + + loadPerms(): Promise { + if (this.permissions) { + return Promise.resolve(); + } + return this.perm.hasWorkPermAt(['ADMIN_PROVIDER', 'MANAGE_PROVIDER', 'VIEW_PROVIDER'], true).then(permMap => { + this.permissions = permMap; + this.viewOUs.concat(permMap['VIEW_PROVIDER']); + this.permissions['ADMIN_PROVIDER'].forEach(ou => { + if (!this.viewOUs.includes(ou)) { + this.viewOUs.push(ou); + } + }); + this.permissions['MANAGE_PROVIDER'].forEach(ou => { + if (!this.viewOUs.includes(ou)) { + this.viewOUs.push(ou); + } + }); + }); + } + + getProviderRecord(id: number): Observable { + console.debug('fetching provider ' + id); + this.currentProviderId = id; + const emptyGuard = this.idl.create('acqpro'); + emptyGuard.id('no_provider_fetched'); + return this.pcrud.search('acqpro', { id: id }, + { + flesh: 3, + flesh_fields: { acqpro: [ + 'attributes', 'holdings_subfields', 'contacts', + 'addresses', 'provider_notes', + 'edi_accounts', 'currency_type', 'edi_default' + ], + acqpa: ['provider'], + acqpc: ['provider', 'addresses'], + acqphsm: ['provider'], + acqlipad: ['provider'], + acqedi: ['attr_set', 'provider'], + } + }, + {} + ).pipe(defaultIfEmpty(emptyGuard), map(acqpro => { + if (acqpro.id() === 'no_provider_fetched') { + throw new Error('no provider to fetch'); + } + const provider = new ProviderRecord(acqpro); + // make a copy of holding_tag for use by the holdings definitions tab + acqpro['_holding_tag'] = acqpro.holding_tag(); + acqpro.edi_accounts().forEach(acct => { + acct['_is_default'] = false; + if (acqpro.edi_default()) { + if (acct.id() === acqpro.edi_default().id()) { + acct['_is_default'] = true; + } + } + }); + acqpro.contacts().forEach(acct => { + acct['_is_primary'] = false; + if (acqpro.primary_contact()) { + if (acct.id() === acqpro.primary_contact()) { + acct['_is_primary'] = true; + } + } + }); + this.currentProvider = provider; + this.checkIfCanDelete(provider); + this.checkIfCanAdmin(provider); + return provider; + })); + } + + checkIfCanDelete(prov: ProviderRecord) { + this.pcrud.search('acqpo', { provider: prov.id }, { limit: 1 }).toPromise() + .then(acqpo => { + if (!acqpo || acqpo.length === 0) { + this.pcrud.search('jub', { provider: prov.id }, { limit: 1 }).toPromise() + .then(jub => { + if (!jub || jub.length === 0) { + this.pcrud.search('acqinv', { provider: prov.id }, { limit: 1 }).toPromise() + .then(acqinv => { + prov.canDelete = true; + }); + } + }); + } + }); + } + + checkIfCanAdmin(prov: ProviderRecord) { + this.loadPerms().then(x => { + if (Object.keys(this.permissions).length > 0 && + this.permissions['ADMIN_PROVIDER'].includes(prov.record.owner())) { + prov.canAdmin = true; + } + }); + } + + checkIfCanAdminAtAll(): boolean { + if (typeof this.permissions === 'undefined') { + return false; + } + if (Object.keys(this.permissions).length > 0 && + this.permissions['ADMIN_PROVIDER'].length > 0) { + return true; + } else { + return false; + } + } + + getViewOUs(): number[] { + return this.viewOUs; + } + + current(): IdlObject { + return this.currentProvider ? this.currentProvider.record : null; + } + currentProviderRecord(): ProviderRecord { + return this.currentProvider ? this.currentProvider : null; + } + + fetch(id: number): Promise { + return new Promise((resolve, reject) => { + this.getProviderRecord(id).subscribe( + result => { + resolve(); + }, + error => { + reject(); + }, + ); + }); + } + + refreshCurrent(): Promise { + if (this.currentProviderId) { + return this.fetch(this.currentProviderId); + } else { + return Promise.reject(); + } + } + + batchUpdate(list: IdlObject | IdlObject[]): Observable { + return this.pcrud.autoApply(list); + } + + announceProviderUpdated() { + this.providerUpdatedSource.next(this.currentProviderId); + } + +} diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-results.component.html b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-results.component.html new file mode 100644 index 0000000000..c3ce484417 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-results.component.html @@ -0,0 +1,25 @@ + + + +
    +
  • {{c.name()}}
  • +
+
+ + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-results.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-results.component.ts new file mode 100644 index 0000000000..2851a2c535 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-results.component.ts @@ -0,0 +1,83 @@ +import {Component, OnInit, AfterViewInit, Input, Output, EventEmitter, ViewChild, ElementRef} from '@angular/core'; +import {Observable} from 'rxjs'; +import {map} from 'rxjs/operators'; +import {Router, ActivatedRoute, ParamMap} from '@angular/router'; +import {Pager} from '@eg/share/util/pager'; +import {IdlObject} from '@eg/core/idl.service'; +import {NetService} from '@eg/core/net.service'; +import {AuthService} from '@eg/core/auth.service'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid'; +import {AcqProviderSearchService, AcqProviderSearchTerm, AcqProviderSearch} from './acq-provider-search.service'; +import {AcqProviderSearchFormComponent} from './acq-provider-search-form.component'; + +@Component({ + selector: 'eg-provider-results', + templateUrl: 'provider-results.component.html', + providers: [AcqProviderSearchService] +}) +export class ProviderResultsComponent implements OnInit, AfterViewInit { + + gridSource: GridDataSource; + @ViewChild('acqSearchProviderGrid', { static: true }) providerResultsGrid: GridComponent; + @ViewChild('providerSearchForm', { static: true }) providerSearchForm: AcqProviderSearchFormComponent; + + cellTextGenerator: GridCellTextGenerator; + @Output() previewRow: (row: any, updateSummaryOnly?: boolean) => void; + @Output() desireSummarize: EventEmitter = new EventEmitter(); + @Output() desireSummaryOnly: EventEmitter = new EventEmitter(); + + constructor( + private elementRef: ElementRef, + private router: Router, + private route: ActivatedRoute, + private net: NetService, + private auth: AuthService, + private providerSearch: AcqProviderSearchService) { + } + + ngOnInit() { + this.gridSource = this.providerSearch.getDataSource(); + + this.cellTextGenerator = { + provider: row => row.provider().code(), + name: row => row.name(), + }; + + this.previewRow = (row: any, updateSummaryOnly = false) => { + if (updateSummaryOnly) { + this.desireSummaryOnly.emit(row.id()); + } else { + this.desireSummarize.emit(row.id()); + } + }; + } + + ngAfterViewInit() { + // check if we're visible; if we are, we've + // likely come in directly from the main Provider Search + // menu item and should go ahead and submit the + // form with default values + // see: https://stackoverflow.com/questions/37843907/angular2-is-there-a-way-to-know-when-a-component-is-hidden + const elm = this.elementRef.nativeElement; + if (elm.offsetParent !== null) { + setTimeout(x => this.providerSearchForm.submitSearch()); + } + } + + retrieveRow(rows: IdlObject[]) { + this.desireSummarize.emit(rows[0].id()); + } + + resetSearch() { + this.providerSearchForm.clearSearch(); + setTimeout(x => this.providerSearchForm.submitSearch()); + } + + doSearch(search: AcqProviderSearch) { + setTimeout(() => { + this.providerSearch.setSearch(search); + this.providerResultsGrid.reload(); + }); + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/resolver.service.ts b/Open-ILS/src/eg2/src/app/staff/acq/provider/resolver.service.ts new file mode 100644 index 0000000000..f993b471ba --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/resolver.service.ts @@ -0,0 +1,57 @@ +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; +import {Router, Resolve, RouterStateSnapshot, + ActivatedRouteSnapshot, CanDeactivate} from '@angular/router'; +import {ProviderRecordService} from './provider-record.service'; + +@Injectable() +export class ProviderResolver implements Resolve> { + + savedId: number = null; + + constructor( + private router: Router, + private providerRecord: ProviderRecordService, + ) {} + + resolve( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot): Promise { + + console.debug('ProviderResolver:resolve()'); + + const id = Number(route.paramMap.get('id')); + + if (this.savedId !== null && this.savedId === id) { + // don't refetch + return Promise.all([ + Promise.resolve(), + ]); + } else { + this.savedId = id; + return Promise.all([ + this.providerRecord.fetch(id).then( + ok => { + console.debug(this.providerRecord.current()); + }, + err => { + this.router.navigate(['/staff', 'acq', 'provider']); + } + ), + ]); + } + } + +} + +// following example of https://www.concretepage.com/angular-2/angular-candeactivate-guard-example +export interface DeactivationGuarded { + canDeactivate(): Observable | Promise | boolean; +} + +@Injectable() +export class CanLeaveAcqProviderGuard implements CanDeactivate { + canDeactivate(component: DeactivationGuarded): Observable | Promise | boolean { + return component.canDeactivate ? component.canDeactivate() : true; + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/acq/provider/routing.module.ts new file mode 100644 index 0000000000..c8e0686816 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/routing.module.ts @@ -0,0 +1,30 @@ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {AcqProviderComponent} from './acq-provider.component'; +import {ProviderResolver, CanLeaveAcqProviderGuard} from './resolver.service'; + +const routes: Routes = [ + { path: '', + component: AcqProviderComponent, + runGuardsAndResolvers: 'always' + }, + { path: ':id', + component: AcqProviderComponent, + resolve: { providerResolver : ProviderResolver }, + runGuardsAndResolvers: 'always' + }, + { path: ':id/:tab', + component: AcqProviderComponent, + resolve: { providerResolver : ProviderResolver }, + canDeactivate: [CanLeaveAcqProviderGuard], + runGuardsAndResolvers: 'always' + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ProviderResolver, CanLeaveAcqProviderGuard] +}) + +export class AcqProviderRoutingModule {} diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/summary-pane.component.css b/Open-ILS/src/eg2/src/app/staff/acq/provider/summary-pane.component.css new file mode 100644 index 0000000000..de3c24bf50 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/summary-pane.component.css @@ -0,0 +1,27 @@ +#acq-provider-summary-pane { + border-radius: 0px 0px 7px 7px; + background-color: rgb(247, 247, 247); + box-shadow: 1px 2px 3px -1px rgba(0, 0, 0, .2); +} +.row { border-bottom: dotted thin; } +.row.provider_id { } +.row.provider_name { } +.row.provider_code { } +.row.provider_owner { } +.row.provider_currency_type { } +.row.provider_holding_tag { display: none; } +.row.provider_addresses { display: none; } +.row.provider_san { } +.row.provider_edi_default { } +.row.provider_active { } +.row.provider_prepayment_required { display: none; } +.row.provider_url { } +.row.provider_email { display: none; } +.row.provider_phone { display: none; } +.row.provider_fax_phone { display: none; } +.row.provider_default_claim_policy { display: none; } +.row.provider_default_copy_count { display: none; } +.row.provider_contacts { } +.provider_contact_role { font-style: italic; } +.provider_primary_contact { color: red; } +.row.provider_provider_notes { display: none; } diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/summary-pane.component.html b/Open-ILS/src/eg2/src/app/staff/acq/provider/summary-pane.component.html new file mode 100644 index 0000000000..e3e848b458 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/summary-pane.component.html @@ -0,0 +1,125 @@ + + + +
+ +Provider Deletion Failed + + + + + + +
+
{{provider_name_label}}
+
{{provider_name}}
+
+ +
+
{{provider_code_label}}
+
{{provider_code}}
+
+ +
+
{{provider_owner_label}}
+
{{provider_owner}}
+
+ +
+
{{provider_id_label}}
+
{{provider_id}}
+
+ +
+
{{provider_currency_type_label}}
+
{{provider_currency_type}}
+
+ +
+
{{provider_contacts_label}}
+
+
+ {{contact.role()}} : {{contact.name()}} + {{contact.name()}} + (primary) +
+
+
+ +
+
{{provider_san_label}}
+
{{provider_san}}
+
+ +
+
{{provider_edi_default_label}}
+
{{provider_edi_default}}
+
+ +
+
{{provider_url_label}}
+ +
+ +
+
{{provider_holding_tag_label}}
+
{{provider_holding_tag}}
+
+ +
+
{{provider_addresses_label}}
+
{{provider_addresses}}
+
+ +
+
{{provider_active_label}}
+
+
+ +
+
{{provider_prepayment_required_label}}
+
{{provider_prepayment_required}}
+
+ +
+
{{provider_email_label}}
+
{{provider_email}}
+
+ +
+
{{provider_phone_label}}
+
{{provider_phone}}
+
+ +
+
{{provider_fax_phone_label}}
+
{{provider_fax_phone}}
+
+ +
+
{{provider_default_claim_policy_label}}
+
{{provider_default_claim_policy}}
+
+ +
+
{{provider_default_copy_count_label}}
+
{{provider_default_copy_count}}
+
+ +
+
{{provider_provider_notes_label}}
+
{{provider_provider_notes}}
+
+ +
+ + diff --git a/Open-ILS/src/eg2/src/app/staff/acq/provider/summary-pane.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/provider/summary-pane.component.ts new file mode 100644 index 0000000000..0e843327fb --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/summary-pane.component.ts @@ -0,0 +1,211 @@ +import {Component, OnInit, AfterViewInit, Input, Output, EventEmitter, ViewChild} from '@angular/core'; +import {Router} from '@angular/router'; +import {StaffCommonModule} from '@eg/staff/common.module'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {OrgService} from '@eg/core/org.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component'; +import {AuthService} from '@eg/core/auth.service'; +import {ProviderRecord, ProviderRecordService} from './provider-record.service'; + +@Component({ + selector: 'eg-acq-provider-summary-pane', + styleUrls: ['summary-pane.component.css'], + templateUrl: './summary-pane.component.html' +}) + +export class AcqProviderSummaryPaneComponent implements OnInit, AfterViewInit { + + @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent; + + collapsed = false; + + provider_id = ''; + provider_name = ''; + provider_code = ''; + provider_owner = ''; + provider_currency_type = ''; + provider_holding_tag = ''; + provider_addresses = ''; + provider_san = ''; + provider_edi_default = ''; + provider_active = ''; + provider_prepayment_required = ''; + provider_url = ''; + provider_email = ''; + provider_phone = ''; + provider_fax_phone = ''; + provider_default_claim_policy = ''; + provider_default_copy_count = ''; + provider_contacts = ''; + provider_provider_notes = ''; + + provider_id_label; + provider_name_label; + provider_code_label; + provider_owner_label; + provider_currency_type_label; + provider_holding_tag_label; + provider_addresses_label; + provider_san_label; + provider_edi_default_label; + provider_active_label; + provider_prepayment_required_label; + provider_url_label; + provider_email_label; + provider_phone_label; + provider_fax_phone_label; + provider_default_claim_policy_label; + provider_default_copy_count_label; + provider_contacts_label; + provider_provider_notes_label; + + @Input() providerId: any; + @ViewChild('errorString', { static: true }) errorString: StringComponent; + @ViewChild('delConfirm', { static: true }) delConfirm: ConfirmDialogComponent; + @Output() summaryToggled: EventEmitter = new EventEmitter(); + + provider: IdlObject; + provRec: ProviderRecord; + + constructor( + private router: Router, + private pcrud: PcrudService, + private idl: IdlService, + private org: OrgService, + private toast: ToastService, + private auth: AuthService, + private prov: ProviderRecordService, + ) {} + + ngOnInit() { + this.provider_id_label = this.idl.classes['acqpro'].field_map['id'].label; + this.provider_name_label = this.idl.classes['acqpro'].field_map['name'].label; + this.provider_code_label = this.idl.classes['acqpro'].field_map['code'].label; + this.provider_owner_label = this.idl.classes['acqpro'].field_map['owner'].label; + this.provider_currency_type_label = this.idl.classes['acqpro'].field_map['currency_type'].label; + this.provider_holding_tag_label = this.idl.classes['acqpro'].field_map['holding_tag'].label; + this.provider_addresses_label = this.idl.classes['acqpro'].field_map['addresses'].label; + this.provider_san_label = this.idl.classes['acqpro'].field_map['san'].label; + this.provider_edi_default_label = this.idl.classes['acqpro'].field_map['edi_default'].label; + this.provider_active_label = this.idl.classes['acqpro'].field_map['active'].label; + this.provider_prepayment_required_label = this.idl.classes['acqpro'].field_map['prepayment_required'].label; + this.provider_url_label = this.idl.classes['acqpro'].field_map['url'].label; + this.provider_email_label = this.idl.classes['acqpro'].field_map['email'].label; + this.provider_phone_label = this.idl.classes['acqpro'].field_map['phone'].label; + this.provider_fax_phone_label = this.idl.classes['acqpro'].field_map['fax_phone'].label; + this.provider_default_claim_policy_label = this.idl.classes['acqpro'].field_map['default_claim_policy'].label; + this.provider_default_copy_count_label = this.idl.classes['acqpro'].field_map['default_copy_count'].label; + this.provider_contacts_label = this.idl.classes['acqpro'].field_map['contacts'].label; + this.provider_provider_notes_label = this.idl.classes['acqpro'].field_map['provider_notes'].label; + } + + ngAfterViewInit() { + if (this.providerId) { + this.update(this.providerId); + } + } + + update(newProvider: any) { + function no_provider() { + // FIXME: empty the pane or keep last summarized view? + this.provider_id = ''; + this.provider_name = ''; + this.provider_code = ''; + this.provider_owner = ''; + this.provider_currency_type = ''; + this.provider_holding_tag = ''; + this.provider_addresses = ''; + this.provider_san = ''; + this.provider_edi_default = ''; + this.provider_active = ''; + this.provider_prepayment_required = ''; + this.provider_url = ''; + this.provider_email = ''; + this.provider_phone = ''; + this.provider_fax_phone = ''; + this.provider_default_claim_policy = ''; + this.provider_default_copy_count = ''; + this.provider_contacts = ''; + this.provider_provider_notes = ''; + } + + if (newProvider) { + const providerRecord = this.prov.currentProviderRecord(); + const provider = providerRecord.record; + if (provider) { + this.provRec = providerRecord; + this.provider = provider; + this.provider_id = provider.id(); + this.provider_name = provider.name(); + this.provider_code = provider.code(); + this.provider_owner = this.org.get(provider.owner()).shortname(); + this.provider_currency_type = provider.currency_type() ? provider.currency_type().label() : ''; + this.provider_holding_tag = provider.holding_tag(); + this.provider_addresses = provider.addresses(); + this.provider_san = provider.san(); + if (typeof provider.edi_default() === 'object') { + this.provider_edi_default = provider.edi_default() ? provider.edi_default().label() : ''; + } else { + // not fleshed, presumably because user doesn't have + // permission to retrieve EDI accounts + this.provider_edi_default = ''; + } + this.provider_active = provider.active(); + this.provider_prepayment_required = provider.prepayment_required(); + this.provider_url = provider.url(); + this.provider_email = provider.email(); + this.provider_phone = provider.phone(); + this.provider_fax_phone = provider.fax_phone(); + this.provider_default_claim_policy = provider.default_claim_policy(); + this.provider_default_copy_count = provider.default_copy_count(); + this.provider_contacts = provider.contacts(); + this.provider_provider_notes = provider.provider_notes(); + } else { + this.provider = null; + no_provider(); + } + } else { + no_provider(); + } + } + + deleteProvider() { + this.delConfirm.open().subscribe(confirmed => { + if (!confirmed) { return; } + + this.pcrud.remove(this.provider) + .subscribe( + ok2 => { + this.deleteSuccessString.current() + .then(str => this.toast.success(str)); + this.router.navigate(['/staff', 'acq', 'provider']); + }, + err => { + this.errorString.current() + .then(str => this.toast.danger(str)); + }, + () => { + console.log('deleteProvider, what is this?'); + } + ); + }); + + } + + canDeleteProvider() { + if (this.provider && this.provider.id()) { + return this.provRec.canAdmin && this.provRec.canDelete; + } else { + return false; + } + } + + toggleCollapse() { + this.collapsed = ! this.collapsed; + this.summaryToggled.emit(this.collapsed); + } + +} diff --git a/Open-ILS/src/eg2/src/app/staff/acq/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/acq/routing.module.ts index 2230dd71d5..8cde15192f 100644 --- a/Open-ILS/src/eg2/src/app/staff/acq/routing.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/acq/routing.module.ts @@ -5,6 +5,10 @@ const routes: Routes = [ { path: 'search', loadChildren: () => import('./search/acq-search.module').then(m => m.AcqSearchModule) + }, + { path: 'provider', + loadChildren: () => + import('./provider/acq-provider.module').then(m => m.AcqProviderModule) } ]; diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/invoice-results.component.html b/Open-ILS/src/eg2/src/app/staff/acq/search/invoice-results.component.html index 18d8caf70d..25e1ef6c58 100644 --- a/Open-ILS/src/eg2/src/app/staff/acq/search/invoice-results.component.html +++ b/Open-ILS/src/eg2/src/app/staff/acq/search/invoice-results.component.html @@ -10,13 +10,13 @@ - {{invoice.provider().code()}} - {{invoice.shipper().code()}} diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/lineitem-results.component.html b/Open-ILS/src/eg2/src/app/staff/acq/search/lineitem-results.component.html index 2da1a97ff8..d7fc3ab43b 100644 --- a/Open-ILS/src/eg2/src/app/staff/acq/search/lineitem-results.component.html +++ b/Open-ILS/src/eg2/src/app/staff/acq/search/lineitem-results.component.html @@ -36,7 +36,7 @@ - {{lineitem.provider().code()}} diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/purchase-order-results.component.html b/Open-ILS/src/eg2/src/app/staff/acq/search/purchase-order-results.component.html index 5c3c28bde3..f1ca17024f 100644 --- a/Open-ILS/src/eg2/src/app/staff/acq/search/purchase-order-results.component.html +++ b/Open-ILS/src/eg2/src/app/staff/acq/search/purchase-order-results.component.html @@ -11,7 +11,7 @@ - {{purchaseorder.provider().code()}} -- 2.11.0