From 40ddab1a3e2232e9247892a14d72ded450c14a2a Mon Sep 17 00:00:00 2001 From: Galen Charlton Date: Tue, 3 Mar 2020 18:18:55 -0500 Subject: [PATCH] LP#1857150: Angular provider search and management interface The interfaces for searching for and managing acquisitions provider records have been rewritten in Angular. This rewrite includes the following significant changes: * The provider search interface is now available directly from the Acquisitions menu, supplementing its longstanding availability from the Acquisitions Administration page. * The search interface is modeled after the patron interface, including a search form that can be hidden or displayed, a provider summary box, and a multi-tabbed interface for managing the provider itself. * The grid displaying search results is filterable and sortable. * The provider display tabs are ** Details, allowing the user to view, and if permitted, edit the base provider record. ** Addresses ** Contacts ** Attribute Definitions ** Holdings Definitions ** EDI ** Invoices, providing an interface for viewing the invoices associated with the provider. ** POs, providing an interface for viewing the purchase orders associated with the provider. * The new interface makes it possible to edit contact addresses. * The base provider record now has an optional primary contact field. Selecting a contact as the primary one is managed on the Contacts tab. The primary contact, if set, is displayed on the provider summary box. Interfaces that used to link to the Dojo provider interface now link to the Angular one instead. This patch also includes a mechanism for stopping navigation away from a dirty provider or holdings tag form unless the user expressly confirms that they want to abandon the unsaved changes. This patch includes contributions by Mike Rylander and Jason Etheridge. Sponsored-by: Evergreen Community Development Initiative Signed-off-by: Galen Charlton Signed-off-by: Ruth Frasur Signed-off-by: Bill Erickson Signed-off-by: Jason Etheridge --- .../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 | 194 ++++++++++++ .../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 | 328 +++++++++++++++++++++ .../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 | 225 ++++++++++++++ .../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 +- .../admin/acq/admin-acq-splash.component.html | 5 +- Open-ILS/src/eg2/src/app/staff/nav.component.html | 6 + Open-ILS/src/templates/menu.tt2 | 1 + .../src/templates/staff/admin/acq/t_splash.tt2 | 2 +- Open-ILS/src/templates/staff/navbar.tt2 | 6 + 42 files changed, 3792 insertions(+), 9 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..69aebda85c --- /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..895407bc7d --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/acq-provider.component.ts @@ -0,0 +1,194 @@ +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, hideSearchForm?: 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, hideSearchForm = true) => { + 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(); + if (hideSearchForm) { + 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..7fd53c26b3 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-contacts.component.ts @@ -0,0 +1,328 @@ +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 = { + email: row => row.email(), + phone: row => row.phone(), + }; + 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..2c1c2b2311 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/provider/provider-holdings.component.ts @@ -0,0 +1,225 @@ +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 = { + name: row => row.name(), + }; + 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..acd696f48a --- /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..27ef5d381b --- /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, hideSearchForm?: boolean) => void; + @Output() desireSummarize: EventEmitter = new EventEmitter(); + @Output() summarizeSearchFormOpen: 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, hideSearchForm = true) => { + if (hideSearchForm) { + this.desireSummarize.emit(row.id()); + } else { + this.summarizeSearchFormOpen.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()}} diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq-splash.component.html b/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq-splash.component.html index c6d73076d4..c0f8961313 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq-splash.component.html +++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq-splash.component.html @@ -53,10 +53,7 @@ - + routerLink="/staff/acq/provider"> diff --git a/Open-ILS/src/eg2/src/app/staff/nav.component.html b/Open-ILS/src/eg2/src/app/staff/nav.component.html index 372299acb0..ad4a3d8088 100644 --- a/Open-ILS/src/eg2/src/app/staff/nav.component.html +++ b/Open-ILS/src/eg2/src/app/staff/nav.component.html @@ -242,6 +242,12 @@ General Search + + + Provider Search + + diff --git a/Open-ILS/src/templates/menu.tt2 b/Open-ILS/src/templates/menu.tt2 index fab8407da6..62e1fb0c26 100644 --- a/Open-ILS/src/templates/menu.tt2 +++ b/Open-ILS/src/templates/menu.tt2 @@ -79,6 +79,7 @@ onClick="location.href = '[% ctx.base_path %]/acq/funding_source/list';">[% l('Funding Sources') %]
[% l('Providers') %]
+
[% l('Currency Types') %]