];
@NgModule({
- imports: [RouterModule.forRoot(routes)],
+ imports: [RouterModule.forRoot(routes, {
+ onSameUrlNavigation: 'reload'
+ })],
exports: [RouterModule],
providers: [BaseResolver]
})
--- /dev/null
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+
+const routes: Routes = [
+ { path: 'search',
+ loadChildren: () =>
+ import('./search/acq-search.module').then(m => m.AcqSearchModule)
+ }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+
+export class AcqRoutingModule {}
--- /dev/null
+#acq-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);
+}
--- /dev/null
+<eg-string #defaultSearchSavedString i18n-text text="Default search saved"></eg-string>
+<eg-string #defaultSearchResetString i18n-text text="Default search reset"></eg-string>
+
+<div id="acq-search-form" class="pl-3 pr-3 pt-3 pb-3 mb-3">
+<form>
+ <div class="row mb-1">
+ <div class="col form-group form-inline">
+ <label i18n>Search for records matching
+ <select class="form-inline ml-1 mr-1" id="acq-search-conjunction" [ngModelOptions]="{standalone: true}" [(ngModel)]="searchConjunction">
+ <option value="all">all</option>
+ <option value="any">any</option>
+ </select>
+ of the following terms:</label>
+ </div>
+ <div class="col-auto">
+ <a class="with-material-icon no-href text-primary"
+ title="Show Form" i18n-title
+ tabindex="0"
+ *ngIf="!showForm" (click)="showForm=true"><span class="sr-only" i18n>Show Form</span>
+ <span class="material-icons" aria-hidden="true">expand_more</span>
+ </a>
+ <a class="with-material-icon no-href text-primary"
+ title="Hide Form" i18n-title
+ tabindex="0"
+ *ngIf="showForm" (click)="showForm=false"><span class="sr-only" i18n>Hide Form</span>
+ <span class="material-icons" aria-hidden="true">expand_less</span>
+ </a>
+ </div>
+ </div>
+ <div class="row mb-1" *ngFor="let t of searchTerms; let idx=index" [hidden]="!showForm">
+ <div class="col-md-5 col-lg-3">
+ <select class="form-control" id="selected-search-term" [ngModelOptions]="{standalone: true}" [ngModel]="t.field"
+ (ngModelChange)="old = t.field; t.field = $event"
+ (change)="clearSearchTerm(t, old)">
+ <option disabled="disabled" i18n>Select Search Field</option>
+ <optgroup *ngFor="let g of hints" label="{{availableSearchFields[g]['__label']}}">
+ <option *ngFor="let o of availableSearchFields[g]['__fields']" value="{{g}}:{{o}}">
+ {{availableSearchFields[g]['__label']}} - {{availableSearchFields[g][o].label}}
+ </option>
+ </optgroup>
+ </select>
+ </div>
+ <div class="col-xs-2 pl-3">
+ <select class="form-control" id="selected-search-op" [ngModelOptions]="{standalone: true}" [(ngModel)]="t.op"
+ (ngModelChange)="oldOp = t.op; t.op = $event"
+ (change)="clearSearchTermValueAfterOpChange(t, oldOp)">
+ <option i18n value="">is</option>
+ <option i18n value="__not">is NOT</option>
+ <option i18n value="__fuzzy" [hidden]="searchTermDatatypes[t.field] != 'text' && searchFieldLinkedClasses[t.field] !== 'acqpro' && searchFieldLinkedClasses[t.field] !== 'au'">contains</option>
+ <option i18n value="__not,__fuzzy" [hidden]="searchTermDatatypes[t.field] != 'text' && searchFieldLinkedClasses[t.field] !== 'acqpro' && searchFieldLinkedClasses[t.field] !== 'au'">does NOT contain</option>
+ <option i18n value="__starts" [hidden]="searchTermDatatypes[t.field] != 'text'">STARTS with</option>
+ <option i18n value="__ends" [hidden]="searchTermDatatypes[t.field] != 'text'">ENDS with</option>
+ <option i18n value="__lte" [hidden]="searchTermDatatypes[t.field] != 'timestamp' && !dateLikeSearchFields[t.field]">is on or BEFORE</option>
+ <option i18n value="__gte" [hidden]="searchTermDatatypes[t.field] != 'timestamp' && !dateLikeSearchFields[t.field]">is on or AFTER</option>
+ <option i18n value="__between" [hidden]="searchTermDatatypes[t.field] != 'timestamp'">is BETWEEN</option>
+ <option i18n value="__age" [hidden]="searchTermDatatypes[t.field] != 'timestamp'">age (relative date)</option>
+ <option i18n value="__isnotnull" [hidden]="searchTermDatatypes[t.field] == 'id' || searchTermFieldIsRequired[t.field]">exists</option>
+ <option i18n value="__isnull" [hidden]="searchTermDatatypes[t.field] == 'id' || searchTermFieldIsRequired[t.field]">does NOT exist</option>
+ <option i18n value="__in">matches a term from a file</option>
+ </select>
+ </div>
+ <div class="col-sm-3">
+ <ng-container *ngIf="t.op == '__in' || t.op == '__isnull' || t.op == '__isnotnull'">
+ <ng-container *ngIf="t.op == '__in'">
+ <eg-file-reader [(ngModel)]="t.value1" [ngModelOptions]="{standalone: true}"></eg-file-reader>
+ </ng-container>
+ </ng-container>
+ <ng-container *ngIf="t.op !== '__in' && t.op !== '__isnull' && t.op !== '__isnotnull'">
+ <div *ngIf="t.field.endsWith(':state') && (t.op === '' || t.op === '__not'); else notStateField">
+ <eg-combobox *ngIf="t.op != '__fuzzy'"
+ [asyncSupportsEmptyTermClick]="true"
+ [idlClass]="searchFieldLinkedClasses[t.field]"
+ [selectedId]="t.value1"
+ (onChange)="t.value1 = $event ? $event.id : ''">
+ </eg-combobox>
+ </div>
+ <ng-template #notStateField>
+ <input [ngModelOptions]="{standalone: true}" [(ngModel)]="t.value1" type="text" *ngIf="searchTermDatatypes[t.field] == 'id'" class="form-control" />
+ <input [ngModelOptions]="{standalone: true}" [(ngModel)]="t.value1" type="text" *ngIf="searchTermDatatypes[t.field] == 'text'" class="form-control" />
+ <input [ngModelOptions]="{standalone: true}" [(ngModel)]="t.value1" type="number" *ngIf="searchTermDatatypes[t.field] == 'int'" class="form-control" />
+ <input [ngModelOptions]="{standalone: true}" [(ngModel)]="t.value1" type="number" *ngIf="searchTermDatatypes[t.field] == 'money'" class="form-control" />
+ <select [ngModelOptions]="{standalone: true}" [(ngModel)]="t.value1" *ngIf="searchTermDatatypes[t.field] == 'bool'" class="form-control">
+ <option i18n value="t">Yes</option>
+ <option i18n value="f">No</option>
+ </select>
+ <eg-org-select *ngIf="searchTermDatatypes[t.field] == 'org_unit'"
+ [initialOrgId]="t.value1"
+ (onChange)="setOrgUnitSearchValue($event, t)">
+ </eg-org-select>
+ <ng-container *ngIf="searchTermDatatypes[t.field] == 'link'">
+ <ng-container *ngIf="searchFieldLinkedClasses[t.field] === 'acqpro'">
+ <eg-combobox *ngIf="t.op != '__fuzzy' && t.op != '__not,__fuzzy'"
+ [idlClass]="searchFieldLinkedClasses[t.field]"
+ [selectedId]="t.value1"
+ (onChange)="t.value1 = $event ? $event.id : ''">
+ </eg-combobox>
+ <input [ngModelOptions]="{standalone: true}" [(ngModel)]="t.value1" type="text" *ngIf="t.op == '__fuzzy' || t.op == '__not,__fuzzy'" class="form-control" />
+ </ng-container>
+ <ng-container *ngIf="searchFieldLinkedClasses[t.field] === 'au'">
+ <eg-combobox *ngIf="!t.op.includes('__fuzzy')"
+ [idlClass]="searchFieldLinkedClasses[t.field]"
+ [selectedId]="t.value1"
+ (onChange)="t.value1 = $event ? $event.id : ''">
+ </eg-combobox>
+ <input [ngModelOptions]="{standalone: true}" [(ngModel)]="t.value1" type="text" *ngIf="t.op.includes('__fuzzy')" class="form-control" />
+ </ng-container>
+ <ng-container *ngIf="searchFieldLinkedClasses[t.field] !== 'acqpro' && searchFieldLinkedClasses[t.field] !== 'au'">
+ <eg-combobox
+ [asyncSupportsEmptyTermClick]="t.field.endsWith('cancel_reason') || t.field.endsWith(':claim_policy') || t.field.endsWith('_method')"
+ [idlClass]="searchFieldLinkedClasses[t.field]"
+ [selectedId]="t.value1"
+ (onChange)="t.value1 = $event ? $event.id : ''">
+ </eg-combobox>
+ </ng-container>
+ </ng-container>
+ <eg-date-select *ngIf="searchTermDatatypes[t.field] == 'timestamp' && t.op != '__age'"
+ [initialIso]="t.value1"
+ (onChangeAsIso)="t.value1 = $event ? $event : ''; t.is_date = true">
+ </eg-date-select>
+ <ng-container *ngIf="searchTermDatatypes[t.field] == 'timestamp' && t.op == '__between'">
+ <span i18n>and</span>
+ <eg-date-select
+ [initialIso]="t.value2"
+ (onChangeAsIso)="t.value2 = $event ? $event : ''; t.is_date = true">
+ </eg-date-select>
+ </ng-container>
+ <eg-interval-input *ngIf="searchTermDatatypes[t.field] == 'timestamp' && t.op == '__age'"
+ [ngModelOptions]="{standalone: true}" [(ngModel)]="t.value1">
+ </eg-interval-input>
+ </ng-template>
+ </ng-container>
+ </div>
+ <div class="col-xs-2 pl-3 pr-1">
+ <button class="btn btn-sm material-icon-button" type="button"
+ (click)="addSearchTerm()"
+ i18n-title title="Add Search Row"><span class="sr-only">Add Search Row</span>
+ <span class="material-icons" aria-hidden="true">add_circle_outline</span>
+ </button>
+ <button class="btn btn-sm material-icon-button" type="button"
+ (click)="delSearchTerm(idx)"
+ i18n-title title="Remove Search Row"><span class="sr-only">Remove Search Row</span>
+ <span class="material-icons" aria-hidden="true">remove_circle_outline</span>
+ </button>
+ </div>
+ </div>
+ <div class="row" [hidden]="!showForm">
+ <div class="col-sm-2">
+ <button class="btn btn-success" (click)="submitSearch()" type="submit" i18n>Search</button>
+ </div>
+ <div class="col-xs-3"></div>
+ <div class="col-xs-5 pl-3">
+ <button class="btn btn-primary" (click)="saveSearchAsDefault()" type="button" i18n>Set As Default {{searchTypeLabel}} Search</button>
+ <button class="btn btn-secondary" (click)="clearDefaultSearch()" type="button" [disabled]="!hasDefaultSearch" i18n>
+ Reset Default Search
+ </button>
+ </div>
+ <div class="col-xs-3 pl-5">
+ <input class="form-check-input" type="checkbox" id="retrieve-immediately"
+ (change)="saveRunImmediately()"
+ [ngModelOptions]="{standalone: true}" [(ngModel)]="runImmediately"/>
+ <label for="retrieve-immediately" class="form-check-label" i18n>Retrieve Results Immediately</label>
+ </div>
+ </div>
+</form>
+</div>
--- /dev/null
+import {Component, OnInit, AfterViewInit, Input, Output, EventEmitter, ViewChild,
+ OnChanges, SimpleChanges} from '@angular/core';
+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 {AcqSearchTerm, AcqSearch} from './acq-search.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+
+@Component({
+ selector: 'eg-acq-search-form',
+ styleUrls: ['acq-search-form.component.css'],
+ templateUrl: './acq-search-form.component.html'
+})
+
+export class AcqSearchFormComponent implements OnInit, AfterViewInit, OnChanges {
+
+ @Input() initialSearchTerms: AcqSearchTerm[] = [];
+ @Input() fallbackSearchTerms: AcqSearchTerm[] = [];
+ @Input() defaultSearchSetting = '';
+ @Input() runImmediatelySetting = '';
+ @Input() searchTypeLabel = '';
+
+ @Output() searchSubmitted = new EventEmitter<AcqSearch>();
+
+ @ViewChild('defaultSearchSavedString', { static: true}) defaultSearchSavedString: StringComponent;
+ @ViewChild('defaultSearchResetString', { static: true}) defaultSearchResetString: StringComponent;
+
+ showForm = true;
+
+ hints = ['jub', 'acqpl', 'acqpo', 'acqinv', 'acqlid'];
+ availableSearchFields = {};
+ dateLikeSearchFields = {};
+ searchTermDatatypes = {};
+ searchTermFieldIsRequired = {};
+ searchFieldLinkedClasses = {};
+ validSearchTypes = ['lineitems', 'purchaseorders', 'invoices', 'selectionlists'];
+ defaultSearchType = 'lineitems';
+ searchConjunction = 'all';
+ runImmediately = false;
+ hasDefaultSearch = false;
+
+ searchTerms: AcqSearchTerm[] = [];
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private pcrud: PcrudService,
+ private store: ServerStoreService,
+ private idl: IdlService,
+ private toast: ToastService,
+ ) {}
+
+ ngOnInit() {
+ const self = this;
+
+ this.store.getItem(this.runImmediatelySetting).then(val => {
+ this.runImmediately = val;
+
+ this.hints.forEach(
+ function(hint) {
+ const o = {};
+ o['__label'] = self.idl.classes[hint].label;
+ o['__fields'] = [];
+ self.idl.classes[hint].fields.forEach(
+ function(field) {
+ if (!field.virtual) {
+ o['__fields'].push(field.name);
+ o[field.name] = {
+ label: field.label,
+ datatype: field.datatype
+ };
+ self.searchTermDatatypes[hint + ':' + field.name] = field.datatype;
+ self.searchTermFieldIsRequired[hint + ':' + field.name] = field.required;
+ if (field.datatype === 'link') {
+ self.searchFieldLinkedClasses[hint + ':' + field.name] = field.class;
+ }
+ }
+ }
+ );
+ self.availableSearchFields[hint] = o;
+ }
+ );
+
+ this.hints.push('acqlia');
+ this.availableSearchFields['acqlia'] = {'__label': this.idl.classes.acqlia.label, '__fields': []};
+ this.pcrud.retrieveAll('acqliad', {'order_by': {'acqliad': 'id'}})
+ .subscribe(liad => {
+ this.availableSearchFields['acqlia']['__fields'].push('' + liad.id());
+ this.availableSearchFields['acqlia'][liad.id()] = {
+ label: liad.description(),
+ datatype: 'text'
+ };
+ this.searchTermDatatypes['acqlia:' + liad.id()] = 'text';
+ if (liad.code().match(/date/)) {
+ this.dateLikeSearchFields['acqlia:' + liad.id()] = true;
+ }
+ });
+
+ if (this.initialSearchTerms.length > 0) {
+ this.searchTerms = JSON.parse(JSON.stringify(this.initialSearchTerms)); // deep copy
+ this.submitSearch(); // if we've been passed an initial search, e.g., via a URL, assume
+ // we want the results immediately regardless of the workstation
+ // setting
+ } else {
+ this.store.getItem(this.defaultSearchSetting).then(
+ defaultSearch => {
+ if (defaultSearch) {
+ this.searchTerms = JSON.parse(JSON.stringify(defaultSearch.terms));
+ this.searchConjunction = defaultSearch.conjunction;
+ this.hasDefaultSearch = true;
+ } else if (this.fallbackSearchTerms.length) {
+ this.searchTerms.length = 0;
+ JSON.parse(JSON.stringify(this.fallbackSearchTerms))
+ .forEach(term => this.searchTerms.push(term)); // need a copy
+ } else {
+ this.addSearchTerm();
+ }
+ if (this.runImmediately) {
+ if ((this.searchTerms.length > 0) &&
+ (this.searchTerms[0].field !== '')) {
+ this.submitSearch();
+ }
+ }
+ }
+ );
+ }
+ });
+ }
+
+ ngAfterViewInit() {}
+
+ ngOnChanges(changes: SimpleChanges) {
+ if ('initialSearchTerms' in changes && !changes.initialSearchTerms.firstChange) {
+ this.ngOnInit();
+ }
+ }
+
+ addSearchTerm() {
+ this.searchTerms.push({ field: '', op: '', value1: '', value2: '' });
+ }
+ delSearchTerm(index: number) {
+ if (this.searchTerms.length < 2) {
+ this.clearSearchTerm(this.searchTerms[0]);
+ // special case for org_unit
+ if (this.searchTerms[0].field && this.searchTermDatatypes[this.searchTerms[0].field] === 'org_unit') {
+ this.searchTerms = [{ field: this.searchTerms[0].field, op: this.searchTerms[0].op, value1: '', value2: ''}];
+ }
+ // and timestamps
+ if (this.searchTerms[0].field && this.searchTermDatatypes[this.searchTerms[0].field] === 'timestamp') {
+ this.searchTerms = [{ field: this.searchTerms[0].field, op: this.searchTerms[0].op, value1: '', value2: ''}];
+ }
+ } else {
+ this.searchTerms.splice(index, 1);
+ }
+ }
+ clearSearchTerm(term: AcqSearchTerm, old?) {
+ // work around fact that org selector doesn't implement ngModel
+ // and we don't use it for eg-date-select
+ if (old && this.searchTermDatatypes[old] === this.searchTermDatatypes[term.field] &&
+ (this.searchTermDatatypes[old] === 'org_unit' || this.searchTermDatatypes[old] === 'timestamp')) {
+ // don't change values if we're moving from one
+ // org_unit or timestamp field to another
+ } else {
+ term.value1 = '';
+ term.value2 = '';
+ term.is_date = false;
+ }
+
+ // handle change of field type
+ if (old && this.searchTermDatatypes[old] !== this.searchTermDatatypes[term.field]) {
+ term.op = '';
+ }
+ if (old && this.searchTermDatatypes[old] === this.searchTermDatatypes[term.field] &&
+ this.searchTermDatatypes[term.field] === 'link' &&
+ (this.searchFieldLinkedClasses[old] !== this.searchFieldLinkedClasses[term.field])
+ ) {
+ term.op = '';
+ }
+ if (term.field.startsWith('acqlia:') && term.op === '') {
+ // default operator for line item attributes should be "contains"
+ term.op = '__fuzzy';
+ } else if (this.searchTermDatatypes[term.field] !== 'text' && term.op.endsWith('__fuzzy')) {
+ // avoid trying to use the "contains" operator for non-text fields
+ term.op = '';
+ }
+ }
+ // conditionally clear the search term after changing
+ // to selected search operators
+ clearSearchTermValueAfterOpChange(term: AcqSearchTerm, oldOp?) {
+ if (term.op === '__age') {
+ term.value1 = '';
+ term.value2 = '';
+ }
+ if (this.searchTermDatatypes[term.field] === 'link') {
+ if (oldOp === '__fuzzy' || term.op === '__fuzzy' ||
+ oldOp === '__not,__fuzzy' || term.op === '__not,__fuzzy'
+ ) {
+ term.value1 = '';
+ term.value2 = '';
+ }
+ }
+ }
+
+ setOrgUnitSearchValue(org: IdlObject, term: AcqSearchTerm) {
+ if (org == null) {
+ term.value1 = '';
+ } else {
+ term.value1 = org.id();
+ }
+ }
+
+ submitSearch() {
+ // tossing setTimeout here to ensure that the
+ // grid data source is fully initialized
+ setTimeout(() => {
+ this.searchSubmitted.emit({
+ terms: this.searchTerms,
+ conjunction: this.searchConjunction
+ });
+ });
+ }
+
+ saveSearchAsDefault() {
+ return this.store.setItem(this.defaultSearchSetting, {
+ terms: this.searchTerms,
+ conjunction: this.searchConjunction
+ }).then(() => {
+ this.hasDefaultSearch = true;
+ this.defaultSearchSavedString.current().then(msg =>
+ this.toast.success(msg)
+ );
+ });
+ }
+ clearDefaultSearch() {
+ return this.store.removeItem(this.defaultSearchSetting).then(() => {
+ this.hasDefaultSearch = false;
+ this.defaultSearchResetString.current().then(msg =>
+ this.toast.success(msg)
+ );
+ });
+ }
+ saveRunImmediately() {
+ return this.store.setItem(this.runImmediatelySetting, this.runImmediately);
+ }
+}
--- /dev/null
+<eg-staff-banner bannerText="Acquisitions Search" i18n-bannerText>
+</eg-staff-banner>
+
+<div class="row">
+ <div class="ml-auto mr-3"><a i18n href="/eg/staff/acq/legacy/search/unified">Legacy Search Interface</a></div>
+</div>
+<div class="row" id="acq-search-page">
+ <div class="col-lg-12">
+ <ul ngbNav #acqSearchTabs="ngbNav" class="nav-tabs" [(activeId)]="searchType" (navChange)="onTabChange($event)">
+ <li [ngbNavItem]="'lineitems'">
+ <a ngbNavLink i18n>Line Items Search</a>
+ <ng-template ngbNavContent><eg-lineitem-results [initialSearchTerms]="urlSearchTerms"></eg-lineitem-results></ng-template>
+ </li>
+ <li [ngbNavItem]="'purchaseorders'">
+ <a ngbNavLink i18n>Purchase Orders Search</a>
+ <ng-template ngbNavContent><eg-purchase-order-results [initialSearchTerms]="urlSearchTerms"></eg-purchase-order-results></ng-template>
+ </li>
+ <li [ngbNavItem]="'invoices'">
+ <a ngbNavLink i18n>Invoices Search</a>
+ <ng-template ngbNavContent><eg-invoice-results [initialSearchTerms]="urlSearchTerms"></eg-invoice-results></ng-template>
+ </li>
+ <li [ngbNavItem]="'selectionlists'">
+ <a ngbNavLink i18n>Selection Lists Search</a>
+ <ng-template ngbNavContent><eg-picklist-results [initialSearchTerms]="urlSearchTerms"></eg-picklist-results></ng-template>
+ </li>
+ </ul>
+ <div [ngbNavOutlet]="acqSearchTabs"></div>
+ </div>
+</div>
--- /dev/null
+import {Component, OnInit, AfterViewInit, ViewChild, ViewChildren, QueryList, OnDestroy} from '@angular/core';
+import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+import {Router, ActivatedRoute, ParamMap, RouterEvent, NavigationEnd} from '@angular/router';
+import {filter, takeUntil} from 'rxjs/operators';
+import {Subject} from 'rxjs';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {AcqSearchTerm} from './acq-search.service';
+import {LineitemResultsComponent} from './lineitem-results.component';
+import {PurchaseOrderResultsComponent} from './purchase-order-results.component';
+import {InvoiceResultsComponent} from './invoice-results.component';
+import {PicklistResultsComponent} from './picklist-results.component';
+
+@Component({
+ templateUrl: './acq-search.component.html'
+})
+
+export class AcqSearchComponent implements OnInit, AfterViewInit, OnDestroy {
+
+ searchType = '';
+ validSearchTypes = ['lineitems', 'purchaseorders', 'invoices', 'selectionlists'];
+ defaultSearchType = 'lineitems';
+
+ urlSearchTerms: AcqSearchTerm[] = [];
+
+ onTabChange: ($event: NgbNavChangeEvent) => void;
+ @ViewChild('acqSearchTabs', { static: true }) tabs: NgbNav;
+ @ViewChildren(LineitemResultsComponent) liResults: QueryList<PurchaseOrderResultsComponent>;
+ @ViewChildren(PurchaseOrderResultsComponent) poResults: QueryList<PurchaseOrderResultsComponent>;
+ @ViewChildren(InvoiceResultsComponent) invResults: QueryList<PurchaseOrderResultsComponent>;
+ @ViewChildren(PicklistResultsComponent) plResults: QueryList<PicklistResultsComponent>;
+
+ previousUrl: string = null;
+ public destroyed = new Subject<any>();
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private pcrud: PcrudService,
+ private idl: IdlService,
+ ) {
+ this.route.queryParamMap.subscribe((params: ParamMap) => {
+ this.urlSearchTerms = [];
+ const fields = params.getAll('f');
+ const ops = params.getAll('op');
+ const values1 = params.getAll('val1');
+ const values2 = params.getAll('val2');
+ fields.forEach((f, idx) => {
+ const term: AcqSearchTerm = {
+ field: f,
+ op: '',
+ value1: '',
+ value2: ''
+ };
+ if (idx < ops.length) {
+ term.op = ops[idx];
+ }
+ if (idx < values1.length) {
+ term.value1 = values1[idx];
+ if (term.value1 === 'null') {
+ // convert the string 'null' to a true
+ // null value, mostly for the benefit of the
+ // open invoices navigation link
+ term.value1 = null;
+ }
+ }
+ if (idx < values2.length) {
+ term.value2 = values2[idx];
+ }
+ this.urlSearchTerms.push(term);
+ this.ngOnInit(); // TODO: probably overkill
+ });
+ });
+ this.router.events.pipe(
+ filter((event: RouterEvent) => event instanceof NavigationEnd),
+ takeUntil(this.destroyed)
+ ).subscribe(routeEvent => {
+ if (routeEvent instanceof NavigationEnd) {
+ // force reset of grid data source if we're navigating from
+ // a search tab to the same search tab
+ if (this.previousUrl != null) {
+ const prevRoute = this.previousUrl.match(/acq\/search\/([a-z]+)/);
+ const newRoute = routeEvent.url.match(/acq\/search\/([a-z]+)/);
+ const prevTab = prevRoute == null ? 'lineitems' : prevRoute[1];
+ const newTab = newRoute == null ? 'lineitems' : newRoute[1];
+ if (prevTab === newTab) {
+ switch (newTab) {
+ case 'lineitems':
+ this.liResults.toArray()[0].gridSource.reset();
+ this.liResults.toArray()[0].acqSearchForm.ngOnInit();
+ break;
+ case 'purchaseorders':
+ this.poResults.toArray()[0].gridSource.reset();
+ this.poResults.toArray()[0].acqSearchForm.ngOnInit();
+ break;
+ case 'invoices':
+ this.invResults.toArray()[0].gridSource.reset();
+ this.invResults.toArray()[0].acqSearchForm.ngOnInit();
+ break;
+ case 'selectionlists':
+ this.plResults.toArray()[0].gridSource.reset();
+ this.plResults.toArray()[0].acqSearchForm.ngOnInit();
+ break;
+ }
+ }
+ }
+ this.previousUrl = routeEvent.url;
+ this.ngOnInit(); // TODO: probably overkill
+ }
+ });
+ }
+
+ ngOnInit() {
+ const self = this;
+
+ const searchTypeParam = this.route.snapshot.paramMap.get('searchtype');
+
+ if (searchTypeParam) {
+ if (this.validSearchTypes.includes(searchTypeParam)) {
+ this.searchType = searchTypeParam;
+ } else {
+ this.searchType = this.defaultSearchType;
+ this.router.navigate(['/staff', 'acq', 'search', this.searchType]);
+ }
+ } else {
+ this.searchType = this.defaultSearchType;
+ }
+
+ this.onTabChange = ($event) => {
+ if (this.validSearchTypes.includes($event.nextId)) {
+ this.searchType = $event.nextId;
+ this.urlSearchTerms = [];
+ this.router.navigate(['/staff', 'acq', 'search', $event.nextId]);
+ }
+ };
+ }
+
+ ngAfterViewInit() {}
+
+ ngOnDestroy(): void {
+ this.destroyed.next();
+ this.destroyed.complete();
+ }
+
+}
--- /dev/null
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {AcqSearchRoutingModule} from './routing.module';
+import {AcqSearchComponent} from './acq-search.component';
+import {AcqSearchFormComponent} from './acq-search-form.component';
+import {LineitemResultsComponent} from './lineitem-results.component';
+import {PurchaseOrderResultsComponent} from './purchase-order-results.component';
+import {InvoiceResultsComponent} from './invoice-results.component';
+import {PicklistResultsComponent} from './picklist-results.component';
+import {PicklistCreateDialogComponent} from './picklist-create-dialog.component';
+import {PicklistCloneDialogComponent} from './picklist-clone-dialog.component';
+import {PicklistDeleteDialogComponent} from './picklist-delete-dialog.component';
+import {PicklistMergeDialogComponent} from './picklist-merge-dialog.component';
+
+@NgModule({
+ declarations: [
+ AcqSearchComponent,
+ AcqSearchFormComponent,
+ LineitemResultsComponent,
+ PurchaseOrderResultsComponent,
+ InvoiceResultsComponent,
+ PicklistResultsComponent,
+ PicklistCreateDialogComponent,
+ PicklistCloneDialogComponent,
+ PicklistDeleteDialogComponent,
+ PicklistMergeDialogComponent
+ ],
+ imports: [
+ StaffCommonModule,
+ AcqSearchRoutingModule
+ ]
+})
+
+export class AcqSearchModule {
+}
--- /dev/null
+import {Injectable} from '@angular/core';
+import {empty, throwError} from 'rxjs';
+import {map} from 'rxjs/operators';
+import {NetService} from '@eg/core/net.service';
+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 {IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {AttrDefsService} from './attr-defs.service';
+
+const baseIdlClass = {
+ lineitem: 'jub',
+ purchase_order: 'acqpo',
+ picklist: 'acqpl',
+ invoice: 'acqinv'
+};
+
+const defaultSearch = {
+ lineitem: {
+ jub: [{
+ id: '0',
+ __gte: true
+ }]
+ },
+ purchase_order: {
+ acqpo: [{
+ id: '0',
+ __gte: true
+ }]
+ },
+ picklist: {
+ acqpl: [{
+ id: '0',
+ __gte: true
+ }]
+ },
+ invoice: {
+ acqinv: [{
+ id: '0',
+ __gte: true
+ }]
+ },
+};
+
+const searchOptions = {
+ lineitem: {
+ flesh_attrs: true,
+ flesh_cancel_reason: true,
+ flesh_notes: true,
+ flesh_provider: true,
+ flesh_claim_policy: true,
+ flesh_queued_record: true,
+ flesh_creator: true,
+ flesh_editor: true,
+ flesh_selector: true,
+ flesh_po: true,
+ flesh_pl: true,
+ },
+ purchase_order: {
+ no_flesh_cancel_reason: true,
+ flesh_provider: true,
+ flesh_owner: true,
+ flesh_creator: true,
+ flesh_editor: true
+ },
+ picklist: {
+ flesh_lineitem_count: true,
+ flesh_owner: true,
+ flesh_creator: true,
+ flesh_editor: true
+ },
+ invoice: {
+ no_flesh_misc: false,
+ flesh_provider: true // and shipper, which is also a provider
+ }
+};
+
+const operatorMap = {
+ '!=': '__not',
+ '>': '__gt',
+ '>=': '__gte',
+ '<=': '__lte',
+ '<': '__lt',
+ 'startswith': '__starts',
+ 'endswith': '__ends',
+ 'like': '__fuzzy',
+};
+
+export interface AcqSearchTerm {
+ field: string;
+ op: string;
+ value1: string;
+ value2: string;
+ is_date?: boolean;
+}
+
+export interface AcqSearch {
+ terms: AcqSearchTerm[];
+ conjunction: string;
+}
+
+@Injectable()
+export class AcqSearchService {
+
+ _terms: AcqSearchTerm[] = [];
+ _conjunction = 'all';
+ firstRun = true;
+
+ constructor(
+ private net: NetService,
+ private evt: EventService,
+ private auth: AuthService,
+ private pcrud: PcrudService,
+ private attrDefs: AttrDefsService
+ ) {
+ this.firstRun = true;
+ }
+
+ setSearch(search: AcqSearch) {
+ this._terms = search.terms;
+ this._conjunction = search.conjunction;
+ this.firstRun = false;
+ }
+
+ generateAcqSearch(searchType, filters): any {
+ const andTerms = JSON.parse(JSON.stringify(defaultSearch[searchType])); // deep copy
+ const orTerms = {};
+ const coreRecType = Object.keys(defaultSearch[searchType])[0];
+
+ // handle supplied search terms
+ this._terms.forEach(term => {
+ if (term.value1 === '' && !(term.op === '__isnull' || term.op === '__isnotnull')) {
+ return;
+ }
+ const searchTerm: Object = {};
+ const recType = term.field.split(':')[0];
+ const searchField = term.field.split(':')[1];
+ if (term.op === '__isnull') {
+ searchTerm[searchField] = null;
+ } else if (term.op === '__isnotnull') {
+ searchTerm[searchField] = { '!=' : null };
+ } else if (term.op === '__between') {
+ searchTerm[searchField] = [term.value1, term.value2];
+ } else {
+ searchTerm[searchField] = term.value1;
+ }
+ if (term.op !== '') {
+ if (term.op === '__not,__fuzzy') {
+ searchTerm['__not'] = true;
+ searchTerm['__fuzzy'] = true;
+ } else {
+ searchTerm[term.op] = true;
+ }
+ }
+ if (term.is_date) {
+ searchTerm['__castdate'] = true;
+ }
+ if (this._conjunction === 'any') {
+ if (!(recType in orTerms)) {
+ orTerms[recType] = [];
+ }
+ orTerms[recType].push(searchTerm);
+ } else {
+ if (!(recType in andTerms)) {
+ andTerms[recType] = [];
+ }
+ andTerms[recType].push(searchTerm);
+ }
+ });
+
+ // 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 => {
+ const searchTerm: Object = {};
+ let filterOp = '=';
+ let filterVal = '';
+ if (Object.keys(condition).some(x => x === '-not')) {
+ filterOp = Object.keys(condition['-not'][filterField])[0];
+ filterVal = condition['-not'][filterField][filterOp];
+ searchTerm['__not'] = true;
+ } else {
+ filterOp = Object.keys(condition[filterField])[0];
+ filterVal = condition[filterField][filterOp];
+ if (filterOp === 'like' && filterVal.length > 1) {
+ if (filterVal[0] === '%' && filterVal[filterVal.length - 1] === '%') {
+ filterVal = filterVal.slice(1, filterVal.length - 1);
+ } else if (filterVal[filterVal.length - 1] === '%') {
+ filterVal = filterVal.slice(0, filterVal.length - 1);
+ filterOp = 'startswith';
+ } else if (filterVal[0] === '%') {
+ filterVal = filterVal.slice(1);
+ filterOp = 'endswith';
+ }
+ }
+ }
+
+ if (filterOp in operatorMap) {
+ searchTerm[operatorMap[filterOp]] = true;
+ }
+ if ((['title', 'author'].indexOf(filterField) > -1) &&
+ (filterField in this.attrDefs.attrDefs)) {
+ if (!('acqlia' in andTerms)) {
+ andTerms['acqlia'] = [];
+ }
+ searchTerm[this.attrDefs.attrDefs[filterField].id()] = filterVal;
+ andTerms['acqlia'].push(searchTerm);
+ } else {
+ searchTerm[filterField] = filterVal;
+ andTerms[coreRecType].push(searchTerm);
+ }
+ });
+ });
+ return { andTerms: andTerms, orTerms: orTerms };
+ }
+
+ getAcqSearchDataSource(searchType: string): GridDataSource {
+ const gridSource = new GridDataSource();
+
+ 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 currentSearch = this.generateAcqSearch(searchType, gridSource.filters);
+
+ const opts = { ...searchOptions[searchType] };
+ opts['offset'] = pager.offset;
+ opts['limit'] = pager.limit;
+ opts['au_by_id'] = true;
+
+ if (sort.length > 0) {
+ opts['order_by'] = [];
+ sort.forEach(sort_clause => {
+ if (searchType === 'lineitem' &&
+ ['title', 'author'].indexOf(sort_clause.name) > -1) {
+ opts['order_by'].push({
+ class: 'acqlia',
+ field: 'attr_value',
+ direction: sort_clause.dir
+ });
+ opts['order_by_attr'] = sort_clause.name;
+ } else {
+ opts['order_by'].push({
+ class: baseIdlClass[searchType],
+ field: sort_clause.name,
+ direction: sort_clause.dir
+ });
+ }
+ });
+ }
+
+ return this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.' + searchType + '.unified_search',
+ this.auth.token(),
+ currentSearch.andTerms,
+ currentSearch.orTerms,
+ null,
+ opts
+ ).pipe(
+ map(res => {
+ if (this.evt.parse(res)) {
+ throw throwError(res);
+ } else {
+ return res;
+ }
+ }),
+ );
+ };
+ return gridSource;
+ }
+}
--- /dev/null
+import {Injectable} from '@angular/core';
+import {empty, throwError} from 'rxjs';
+import {map} from 'rxjs/operators';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {IdlObject} from '@eg/core/idl.service';
+
+@Injectable()
+export class AttrDefsService {
+
+ attrDefs: {[code: string]: IdlObject};
+
+ constructor(
+ private pcrud: PcrudService
+ ) {
+ this.attrDefs = {};
+ }
+
+ fetchAttrDefs(): Promise<void> {
+ if (Object.keys(this.attrDefs).length) {
+ return Promise.resolve();
+ }
+ return new Promise((resolve, reject) => {
+ this.pcrud.retrieveAll('acqliad', {},
+ {atomic: true}
+ ).subscribe(list => {
+ list.forEach(acqliad => {
+ this.attrDefs[acqliad.code()] = acqliad;
+ });
+ resolve();
+ });
+ });
+ }
+
+}
--- /dev/null
+<eg-acq-search-form #acqSearchForm (searchSubmitted)="doSearch($event)" [initialSearchTerms]="initialSearchTerms"
+ i18n-searchTypeLabel searchTypeLabel="Invoice" runImmediatelySetting="eg.acq.search.invoices.run_immediately"
+ [fallbackSearchTerms]="fallbackSearchTerms"
+ defaultSearchSetting="eg.acq.search.default.invoices"></eg-acq-search-form>
+
+<ng-template #inv_identTmpl let-invoice="row">
+ <a href="/eg/staff/acq/legacy/invoice/view/{{invoice.id()}}"
+ target="_blank">
+ {{invoice.inv_ident()}}
+ </a>
+</ng-template>
+<ng-template #providerTmpl let-invoice="row">
+ <a href="/eg/staff/admin/acq/conify/provider/{{invoice.provider().id()}}"
+ target="_blank">
+ {{invoice.provider().code()}}
+ </a>
+</ng-template>
+<ng-template #shipperTmpl let-invoice="row">
+ <a href="/eg/staff/admin/acq/conify/provider/{{invoice.shipper().id()}}"
+ target="_blank">
+ {{invoice.shipper().code()}}
+ </a>
+</ng-template>
+
+<eg-grid #acqSearchInvoicesGrid
+ persistKey="acq.search.invoices"
+ [stickyHeader]="true"
+ [filterable]="true"
+ [sortable]="true"
+ [cellTextGenerator]="cellTextGenerator"
+ (onRowActivate)="showRow($event)"
+ idlClass="acqinv" [dataSource]="gridSource">
+
+ <eg-grid-toolbar-action label="Print Selected Invoices" i18n-label
+ (onClick)="printSelectedInvoices($event)" [disableOnRows]="noSelectedRows">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-column path="inv_ident" [cellTemplate]="inv_identTmpl"></eg-grid-column>
+ <eg-grid-column path="provider" [cellTemplate]="providerTmpl"></eg-grid-column>
+ <eg-grid-column path="shipper" [cellTemplate]="shipperTmpl"></eg-grid-column>
+
+ <eg-grid-column path="id" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path="recv_date" [hidden]="true"></eg-grid-column>
+ <eg-grid-column [asyncSupportsEmptyTermClick]="true" path="recv_method"></eg-grid-column>
+ <eg-grid-column [asyncSupportsEmptyTermClick]="true" path="payment_method"></eg-grid-column>
+
+</eg-grid>
+
+<eg-alert-dialog #printfail i18n-dialogBody
+ dialogBody="Could not print the selected invoices.">
+</eg-alert-dialog>
--- /dev/null
+import {Component, OnInit, Input, ViewChild} 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 {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 './acq-search.service';
+import {AcqSearchFormComponent} from './acq-search-form.component';
+
+@Component({
+ selector: 'eg-invoice-results',
+ templateUrl: 'invoice-results.component.html',
+ providers: [AcqSearchService]
+})
+export class InvoiceResultsComponent implements OnInit {
+
+ @Input() initialSearchTerms: AcqSearchTerm[] = [];
+
+ gridSource: GridDataSource;
+ @ViewChild('acqSearchForm', { static: true}) acqSearchForm: AcqSearchFormComponent;
+ @ViewChild('acqSearchInvoicesGrid', { static: true }) invoiceResultsGrid: GridComponent;
+ @ViewChild('printfail', { static: true }) private printfail: AlertDialogComponent;
+
+ noSelectedRows: (rows: IdlObject[]) => boolean;
+
+ cellTextGenerator: GridCellTextGenerator;
+
+ fallbackSearchTerms: AcqSearchTerm[] = [{
+ field: 'acqinv:receiver',
+ op: '',
+ value1: this.auth.user() ? this.auth.user().ws_ou() : '',
+ value2: ''
+ }, {
+ field: 'acqinv:close_date',
+ op: '__isnull',
+ value1: null,
+ value2: ''
+ }];
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private printer: PrintService,
+ private evt: EventService,
+ private net: NetService,
+ private auth: AuthService,
+ 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(),
+ };
+ }
+
+ printSelectedInvoices(rows: IdlObject[]) {
+ const that = this;
+ let html = '<style type="text/css">.acq-invoice-' +
+ 'voucher {page-break-after:always;}' +
+ '</style>\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'
+ })
+ );
+ }
+
+ showRow(row: any) {
+ window.open('/eg/staff/acq/legacy/invoice/view/' + row.id(), '_blank');
+ }
+
+ doSearch(search: AcqSearch) {
+ setTimeout(() => {
+ this.acqSearch.setSearch(search);
+ this.invoiceResultsGrid.reload();
+ });
+ }
+}
--- /dev/null
+<eg-acq-search-form #acqSearchForm (searchSubmitted)="doSearch($event)" [initialSearchTerms]="initialSearchTerms"
+ i18n-searchTypeLabel searchTypeLabel="Line Item" runImmediatelySetting="eg.acq.search.lineitems.run_immediately"
+ defaultSearchSetting="eg.acq.search.default.lineitems"></eg-acq-search-form>
+
+<ng-template #idTmpl let-lineitem="row">
+ <a *ngIf="lineitem.purchase_order()" href="/eg/staff/acq/legacy/po/view/{{lineitem.purchase_order().id()}}?focus_li={{lineitem.id()}}"
+ target="_blank">
+ {{lineitem.id()}}
+ </a>
+ <a *ngIf="lineitem.picklist() && !lineitem.purchase_order()" href="/eg/staff/acq/legacy/picklist/view/{{lineitem.picklist().id()}}?focus_li={{lineitem.id()}}"
+ target="_blank">
+ {{lineitem.id()}}
+ </a>
+</ng-template>
+
+<ng-template #poTmpl let-lineitem="row">
+ <a *ngIf="lineitem.purchase_order()" href="/eg/staff/acq/legacy/po/view/{{lineitem.purchase_order().id()}}?focus_li={{lineitem.id()}}"
+ target="_blank">
+ {{lineitem.purchase_order().name()}}
+ </a>
+</ng-template>
+
+<ng-template #plTmpl let-lineitem="row">
+ <a *ngIf="lineitem.picklist()" href="/eg/staff/acq/legacy/picklist/view/{{lineitem.picklist().id()}}?focus_li={{lineitem.id()}}"
+ target="_blank">
+ {{lineitem.picklist().name()}}
+ </a>
+</ng-template>
+
+<ng-template #liAttrTmpl let-lineitem="row" let-col="col">
+ <ng-container *ngFor="let lia of lineitem.attributes()">
+ <ng-container *ngIf="lia.attr_name() === col.path">
+ {{lia.attr_value()}}
+ </ng-container>
+ </ng-container>
+</ng-template>
+
+<ng-template #providerTmpl let-lineitem="row">
+ <a *ngIf="lineitem.provider()" href="/eg/staff/admin/acq/conify/provider/{{lineitem.provider().id()}}"
+ target="_blank">
+ {{lineitem.provider().code()}}
+ </a>
+</ng-template>
+
+<ng-template #liLinksTmpl let-lineitem="row">
+ <ul>
+ <li *ngIf="lineitem.eg_bib_id()">
+ <a routerLink="/staff/catalog/record/{{lineitem.eg_bib_id()}}"
+ target="_blank" i18n>Catalog</a></li>
+ <li><a href="/eg/staff/acq/legacy/lineitem/worksheet/{{lineitem.id()}}"
+ target="_blank" i18n>Worksheet</a></li>
+ <li *ngIf="lineitem.purchase_order()">
+ <a href="/eg/staff/acq/legacy/po/view/{{lineitem.purchase_order().id()}}"
+ target="_blank" i18n>Purchase Order</a></li>
+ <li><a href="/eg/staff/acq/requests/lineitem/{{lineitem.id()}}"
+ target="_blank" i18n>Requests</a></li>
+ <li>
+ <a routerLink="/staff/acq/search/invoices" [queryParams]="{f: 'jub:id', val1: lineitem.id()}"
+ target="_blank" i18n>Invoices</a></li>
+ <li *ngIf="lineitem.queued_record()">
+ <a routerLink="/staff/cat/vandelay/queue/bib/{{lineitem.queued_record().queue()}}"
+ target="_blank" i18n>Queue</a></li>
+ <li *ngIf="lineitem.picklist()">
+ <a href="/eg/staff/acq/legacy/picklist/view/{{lineitem.picklist().id()}}"
+ target="_blank" i18n>Selection List</a></li>
+ </ul>
+</ng-template>
+
+<eg-grid #acqSearchLineitemsGrid
+ persistKey="acq.search.lineitems"
+ idlClass="jub" [dataSource]="gridSource"
+ ignoreFields="marc"
+ [stickyHeader]="true"
+ [filterable]="true"
+ [sortable]="true"
+ [cellTextGenerator]="cellTextGenerator"
+ (onRowActivate)="showRow($event)"
+ [showDeclaredFieldsOnly]="true">
+
+ <eg-grid-column path="id" [cellTemplate]="idTmpl" [disableTooltip]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Title" path="title" [cellTemplate]="liAttrTmpl"></eg-grid-column>
+ <eg-grid-column i18n-label label="Author" path="author" [cellTemplate]="liAttrTmpl"></eg-grid-column>
+ <eg-grid-column path="provider" [cellTemplate]="providerTmpl"></eg-grid-column>
+ <eg-grid-column i18n-label label="Links" path="_links" [cellTemplate]="liLinksTmpl" [disableTooltip]="true" [filterable]="false" [sortable]="false"></eg-grid-column>
+ <eg-grid-column [asyncSupportsEmptyTermClick]="true" path="claim_policy" [sortable]="false"></eg-grid-column>
+ <eg-grid-column [asyncSupportsEmptyTermClick]="true" i18n-label label="Status" path="state" [disableTooltip]="true"></eg-grid-column>
+ <eg-grid-column path="estimated_unit_price" [disableTooltip]="true"></eg-grid-column>
+ <eg-grid-column path="purchase_order" [cellTemplate]="poTmpl" [disableTooltip]="true" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path="picklist" [cellTemplate]="plTmpl" [disableTooltip]="true" [hidden]="true"></eg-grid-column>
+ <eg-grid-column [asyncSupportsEmptyTermClick]="true" path="cancel_reason" [hidden]="true"></eg-grid-column>
+</eg-grid>
--- /dev/null
+import {Component, OnInit, Input, ViewChild} 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 {AcqSearchService, AcqSearchTerm, AcqSearch} from './acq-search.service';
+import {AcqSearchFormComponent} from './acq-search-form.component';
+
+@Component({
+ selector: 'eg-lineitem-results',
+ templateUrl: 'lineitem-results.component.html',
+ providers: [AcqSearchService]
+})
+export class LineitemResultsComponent implements OnInit {
+
+ @Input() initialSearchTerms: AcqSearchTerm[] = [];
+
+ gridSource: GridDataSource;
+ @ViewChild('acqSearchForm', { static: true}) acqSearchForm: AcqSearchFormComponent;
+ @ViewChild('acqSearchLineitemsGrid', { static: true }) lineitemResultsGrid: GridComponent;
+
+ cellTextGenerator: GridCellTextGenerator;
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private net: NetService,
+ private auth: AuthService,
+ private acqSearch: AcqSearchService) {
+ }
+
+ ngOnInit() {
+ this.gridSource = this.acqSearch.getAcqSearchDataSource('lineitem');
+ this.cellTextGenerator = {
+ id: row => row.id(),
+ title: row => {
+ const filtered = row.attributes().filter(lia => lia.attr_name() === 'title');
+ if (filtered.length > 0) {
+ return filtered[0].attr_value();
+ } else {
+ return '';
+ }
+ },
+ author: row => {
+ const filtered = row.attributes().filter(lia => lia.attr_name() === 'author');
+ if (filtered.length > 0) {
+ return filtered[0].attr_value();
+ } else {
+ return '';
+ }
+ },
+ provider: row => row.provider() ? row.provider().code() : '',
+ _links: row => '',
+ purchase_order: row => row.purchase_order() ? row.purchase_order().name() : '',
+ picklist: row => row.picklist() ? row.picklist().name() : '',
+ };
+ }
+
+ doSearch(search: AcqSearch) {
+ setTimeout(() => {
+ this.acqSearch.setSearch(search);
+ this.lineitemResultsGrid.reload();
+ });
+ }
+
+ showRow(row: any) {
+ window.open('/eg/staff/acq/legacy/lineitem/worksheet/' + row.id(), '_blank');
+ }
+}
--- /dev/null
+<ng-template #dialogContent>
+<form class="form-validated">
+ <div class="modal-header bg-info">
+ <h3 class="modal-title" i18n>Clone Selection List: {{leadListName}}</h3>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="close()">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <h4 i18n>Selection list name:</h4>
+ <input type="text" id="create-picklist-name" required
+ [ngModelOptions]="{standalone: true}"
+ class="form-control" [(ngModel)]="selectionListName">
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-success"
+ [disabled]="!selectionListName"
+ (click)="cloneList()" i18n>Clone</button>
+ <button type="button" class="btn btn-warning"
+ (click)="close()" i18n>Cancel</button>
+ </div>
+</form>
+</ng-template>
+<eg-alert-dialog #fail i18n-dialogBody
+ dialogBody="Could not create this selection list.">
+</eg-alert-dialog>
--- /dev/null
+import {Component, Input, ViewChild, TemplateRef, OnInit, Renderer2} from '@angular/core';
+import {Observable, from, empty, throwError} from 'rxjs';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+@Component({
+ selector: 'eg-picklist-clone-dialog',
+ templateUrl: './picklist-clone-dialog.component.html'
+})
+
+export class PicklistCloneDialogComponent
+ extends DialogComponent implements OnInit {
+
+ @Input() grid: any;
+ selectionListName: String;
+ leadListName: String;
+ selections: IdlObject[];
+
+ @ViewChild('fail', { static: true }) private fail: AlertDialogComponent;
+
+ constructor(
+ private renderer: Renderer2,
+ private idl: IdlService,
+ private evt: EventService,
+ private net: NetService,
+ private auth: AuthService,
+ private modal: NgbModal
+ ) {
+ super(modal);
+ }
+
+ ngOnInit() {
+ }
+
+ update() {
+ this.leadListName = this.grid.context.getSelectedRows()[0].name();
+ this.renderer.selectRootElement('#create-picklist-name').focus();
+ this.selectionListName = 'Copy of ' + this.leadListName;
+ }
+
+ cloneList() {
+ const picklist = this.idl.create('acqpl');
+ picklist.owner(this.auth.user().id());
+ picklist.name(this.selectionListName);
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.picklist.clone',
+ this.auth.token(),
+ this.grid.context.getSelectedRows()[0].id(),
+ this.selectionListName
+ ).subscribe(
+ (res) => {
+ if (this.evt.parse(res)) {
+ console.error(res);
+ this.fail.open();
+ this.close(false);
+ } else {
+ console.log(res);
+ }
+ },
+ (err) => {
+ console.error(err);
+ this.fail.open();
+ this.close(false);
+ },
+ () => this.close(true)
+ );
+ }
+}
+
+
--- /dev/null
+<ng-template #dialogContent>
+<form class="form-validated">
+ <div class="modal-header bg-info">
+ <h3 class="modal-title" i18n>Create New Selection List</h3>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="close()">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <h4 i18n>Selection list name:</h4>
+ <input type="text" id="create-picklist-name" required
+ [ngModelOptions]="{standalone: true}" required
+ class="form-control col-lg-7" [(ngModel)]="selectionListName">
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-success"
+ [disabled]="!selectionListName"
+ (click)="createList()" i18n>Create</button>
+ <button type="button" class="btn btn-warning"
+ (click)="close()" i18n>Cancel</button>
+ </div>
+</form>
+</ng-template>
+<eg-alert-dialog #fail i18n-dialogBody
+ dialogBody="Could not create this selection list.">
+</eg-alert-dialog>
+<eg-alert-dialog #dupe i18n-dialogBody
+ dialogBody="Could not create this selection list: name already in use.">
+</eg-alert-dialog>
--- /dev/null
+import {Component, Input, ViewChild, TemplateRef, OnInit, Renderer2} from '@angular/core';
+import {Observable, from, empty, throwError} from 'rxjs';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+@Component({
+ selector: 'eg-picklist-create-dialog',
+ templateUrl: './picklist-create-dialog.component.html'
+})
+
+export class PicklistCreateDialogComponent
+ extends DialogComponent implements OnInit {
+
+ selectionListName: String;
+
+ @ViewChild('fail', { static: true }) private fail: AlertDialogComponent;
+ @ViewChild('dupe', { static: true }) private dupe: AlertDialogComponent;
+
+ constructor(
+ private renderer: Renderer2,
+ private idl: IdlService,
+ private evt: EventService,
+ private net: NetService,
+ private auth: AuthService,
+ private modal: NgbModal
+ ) {
+ super(modal);
+ }
+
+ ngOnInit() {
+ this.selectionListName = '';
+ }
+
+ update() {
+ this.selectionListName = '';
+ this.renderer.selectRootElement('#create-picklist-name').focus();
+ }
+
+ createList() {
+ const picklist = this.idl.create('acqpl');
+ picklist.owner(this.auth.user().id());
+ picklist.name(this.selectionListName);
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.picklist.create',
+ this.auth.token(), picklist
+ ).subscribe(
+ (res) => {
+ if (this.evt.parse(res)) {
+ console.error(res);
+ if (res.textcode === 'DATABASE_UPDATE_FAILED') {
+ // a duplicate name is not the only reason it could have failed,
+ // but that's the way to bet
+ this.dupe.open();
+ } else {
+ this.fail.open();
+ }
+ this.close(false);
+ } else {
+ console.log(res);
+ }
+ },
+ (err) => {
+ console.error(err);
+ this.fail.open();
+ this.close(false);
+ },
+ () => this.close(true)
+ );
+ }
+}
+
+
--- /dev/null
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <h3 class="modal-title" i18n>Confirm Delete</h3>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="close()">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <h4 i18n>Delete the following selection lists?</h4>
+ <ul>
+ <li *ngFor="let listName of listNames">{{listName}}</li>
+ </ul>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-success"
+ (click)="deleteLists()" i18n>Delete</button>
+ <button type="button" class="btn btn-warning"
+ (click)="close()" i18n>Cancel</button>
+ </div>
+</ng-template>
+<eg-alert-dialog #fail i18n-dialogBody
+ dialogBody="Could not delete the selection list(s).">
+</eg-alert-dialog>
--- /dev/null
+import {Component, Input, ViewChild, TemplateRef, OnInit} from '@angular/core';
+import {Observable, forkJoin, from, empty, throwError} from 'rxjs';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+@Component({
+ selector: 'eg-picklist-delete-dialog',
+ templateUrl: './picklist-delete-dialog.component.html'
+})
+
+export class PicklistDeleteDialogComponent
+ extends DialogComponent implements OnInit {
+
+ @Input() grid: any;
+ listNames: string[];
+
+ @ViewChild('fail', { static: true }) private fail: AlertDialogComponent;
+
+ constructor(
+ private idl: IdlService,
+ private evt: EventService,
+ private net: NetService,
+ private auth: AuthService,
+ private modal: NgbModal
+ ) {
+ super(modal);
+ }
+
+ ngOnInit() {
+ }
+
+ update() {
+ this.listNames = this.grid.context.getSelectedRows().map( r => r.name() );
+ }
+
+ deleteList(list) {
+ return this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.picklist.delete',
+ this.auth.token(),
+ list.id()
+ );
+ }
+
+ deleteLists() {
+ const that = this;
+ const observables = [];
+ this.grid.context.getSelectedRows().forEach(function(r) {
+ observables.push( that.deleteList(r) );
+ });
+ forkJoin(observables).subscribe(
+ (res) => {
+ if (this.evt.parse(res)) {
+ console.error(res);
+ this.fail.open();
+ this.close(false);
+ } else {
+ console.log(res);
+ }
+ },
+ (err) => {
+ console.error(err);
+ this.fail.open();
+ this.close(false);
+ },
+ () => this.close(true)
+ );
+ }
+}
+
+
--- /dev/null
+<ng-template #dialogContent>
+<form class="form-validated">
+ <div class="modal-header bg-info">
+ <h3 class="modal-title" i18n>Merge Selection Lists</h3>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="close()">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <h4 i18n>Lead Selection List:</h4>
+ <select [(ngModel)]="leadList" [ngModelOptions]="{standalone: true}" required>
+ <option *ngFor="let list of selectedLists"
+ value="{{list.id()}}">{{list.name()}}</option>
+ </select>
+ <h4 i18n>Merge the following selection lists?</h4>
+ <ul>
+ <li *ngFor="let listName of listNames">{{listName}}</li>
+ </ul>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-success"
+ [disabled]="!leadList"
+ (click)="mergeLists()" i18n>Merge</button>
+ <button type="button" class="btn btn-warning"
+ (click)="close()" i18n>Cancel</button>
+ </div>
+</form>
+</ng-template>
+<eg-alert-dialog #fail i18n-dialogBody
+ dialogBody="Could not merge the selection lists.">
+</eg-alert-dialog>
--- /dev/null
+import {Component, Input, ViewChild, TemplateRef, OnInit} from '@angular/core';
+import {Observable, forkJoin, from, empty, throwError} from 'rxjs';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+@Component({
+ selector: 'eg-picklist-merge-dialog',
+ templateUrl: './picklist-merge-dialog.component.html'
+})
+
+export class PicklistMergeDialogComponent
+ extends DialogComponent implements OnInit {
+
+ @Input() grid: any;
+ listNames: string[];
+ leadList: number;
+ selectedLists: IdlObject[];
+
+ @ViewChild('fail', { static: true }) private fail: AlertDialogComponent;
+
+ constructor(
+ private idl: IdlService,
+ private evt: EventService,
+ private net: NetService,
+ private auth: AuthService,
+ private modal: NgbModal
+ ) {
+ super(modal);
+ }
+
+ ngOnInit() {
+ }
+
+ update() {
+ this.selectedLists = this.grid.context.getSelectedRows();
+ this.listNames = this.selectedLists.map( r => r.name() );
+ }
+
+ mergeLists() {
+ const that = this;
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.picklist.merge',
+ this.auth.token(), this.leadList,
+ this.selectedLists.map( list => list.id() ).filter(function(p) { return Number(p) !== Number(that.leadList); })
+ ).subscribe(
+ (res) => {
+ if (this.evt.parse(res)) {
+ console.error(res);
+ this.fail.open();
+ this.close(false);
+ } else {
+ console.log(res);
+ }
+ },
+ (err) => {
+ console.error(err);
+ this.fail.open();
+ this.close(false);
+ },
+ () => this.close(true)
+ );
+ }
+
+}
+
+
--- /dev/null
+<eg-acq-search-form #acqSearchForm (searchSubmitted)="doSearch($event)" [initialSearchTerms]="initialSearchTerms"
+ i18n-searchTypeLabel searchTypeLabel="Selection List" runImmediatelySetting="eg.acq.search.selectionlists.run_immediately"
+ [fallbackSearchTerms]="fallbackSearchTerms"
+ defaultSearchSetting="eg.acq.search.default.selectionlists"></eg-acq-search-form>
+
+<eg-string #createSelectionListString i18n-text text="Selection List Created">
+</eg-string>
+<eg-string #cloneSelectionListString i18n-text text="Selection List Cloned">
+</eg-string>
+<eg-string #deleteSelectionListString i18n-text text="Selection List(s) Deleted">
+</eg-string>
+<eg-string #mergeSelectionListString i18n-text text="Selection Lists Merged">
+</eg-string>
+
+<ng-template #nameTmpl let-selectionlist="row">
+ <a href="/eg/staff/acq/legacy/picklist/view/{{selectionlist.id()}}"
+ target="_blank">
+ {{selectionlist.name()}}
+ </a>
+</ng-template>
+
+<eg-picklist-create-dialog #picklistCreateDialog>
+</eg-picklist-create-dialog>
+
+<eg-picklist-clone-dialog #picklistCloneDialog [grid]="picklistResultsGrid">
+</eg-picklist-clone-dialog>
+
+<eg-picklist-delete-dialog #picklistDeleteDialog [grid]="picklistResultsGrid">
+</eg-picklist-delete-dialog>
+
+<eg-picklist-merge-dialog #picklistMergeDialog [grid]="picklistResultsGrid">
+</eg-picklist-merge-dialog>
+
+<eg-grid #acqSearchPicklistsGrid
+ persistKey="acq.search.selectionlists"
+ [stickyHeader]="true"
+ [filterable]="true"
+ [sortable]="true"
+ [cellTextGenerator]="cellTextGenerator"
+ (onRowActivate)="showRow($event)"
+ idlClass="acqpl" [dataSource]="gridSource">
+
+ <eg-grid-toolbar-button label="New Selection List" i18n-label
+ (onClick)="openCreateDialog()" [disableOnRows]="createNotAppropriate">
+ </eg-grid-toolbar-button>
+ <eg-grid-toolbar-action label="Clone Selected" i18n-label
+ (onClick)="openCloneDialog($event)" [disableOnRows]="cloneNotAppropriate">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Merge Selected" i18n-label
+ (onClick)="openMergeDialog($event)" [disableOnRows]="mergeNotAppropriate">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Delete Selected" i18n-label
+ (onClick)="openDeleteDialog($event)" [disableOnRows]="deleteNotAppropriate">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-column path="name" [cellTemplate]="nameTmpl"></eg-grid-column>
+ <eg-grid-column path="entry_count" [filterable]="false" [sortable]="false"></eg-grid-column>
+
+ <eg-grid-column path="id" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path="creator" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path="editor" [hidden]="true"></eg-grid-column>
+
+</eg-grid>
--- /dev/null
+import {Component, OnInit, Input, ViewChild} 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 {ToastService} from '@eg/share/toast/toast.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PermService} from '@eg/core/perm.service';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid';
+import {AcqSearchService, AcqSearchTerm, AcqSearch} from './acq-search.service';
+import {PicklistCreateDialogComponent} from './picklist-create-dialog.component';
+import {PicklistCloneDialogComponent} from './picklist-clone-dialog.component';
+import {PicklistDeleteDialogComponent} from './picklist-delete-dialog.component';
+import {PicklistMergeDialogComponent} from './picklist-merge-dialog.component';
+import {AcqSearchFormComponent} from './acq-search-form.component';
+
+@Component({
+ selector: 'eg-picklist-results',
+ templateUrl: 'picklist-results.component.html',
+ providers: [AcqSearchService]
+})
+export class PicklistResultsComponent implements OnInit {
+
+ @Input() initialSearchTerms: AcqSearchTerm[] = [];
+
+ gridSource: GridDataSource;
+ @ViewChild('acqSearchForm', { static: true}) acqSearchForm: AcqSearchFormComponent;
+ @ViewChild('acqSearchPicklistsGrid', { static: true }) picklistResultsGrid: GridComponent;
+ @ViewChild('picklistCreateDialog', { static: true }) picklistCreateDialog: PicklistCreateDialogComponent;
+ @ViewChild('picklistCloneDialog', { static: true }) picklistCloneDialog: PicklistCloneDialogComponent;
+ @ViewChild('picklistDeleteDialog', { static: true }) picklistDeleteDialog: PicklistDeleteDialogComponent;
+ @ViewChild('picklistMergeDialog', { static: true }) picklistMergeDialog: PicklistMergeDialogComponent;
+ @ViewChild('createSelectionListString', { static: true }) createSelectionListString: StringComponent;
+ @ViewChild('cloneSelectionListString', { static: true }) cloneSelectionListString: StringComponent;
+ @ViewChild('deleteSelectionListString', { static: true }) deleteSelectionListString: StringComponent;
+ @ViewChild('mergeSelectionListString', { static: true }) mergeSelectionListString: StringComponent;
+
+ permissions: {[name: string]: boolean};
+ noSelectedRows: (rows: IdlObject[]) => boolean;
+ oneSelectedRows: (rows: IdlObject[]) => boolean;
+ createNotAppropriate: (rows: IdlObject[]) => boolean;
+ cloneNotAppropriate: (rows: IdlObject[]) => boolean;
+ mergeNotAppropriate: (rows: IdlObject[]) => boolean;
+ deleteNotAppropriate: (rows: IdlObject[]) => boolean;
+
+ cellTextGenerator: GridCellTextGenerator;
+
+ fallbackSearchTerms: AcqSearchTerm[] = [{
+ field: 'acqpl:owner',
+ op: '',
+ value1: this.auth.user() ? this.auth.user().id() : '',
+ value2: ''
+ }];
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private toast: ToastService,
+ private net: NetService,
+ private auth: AuthService,
+ private acqSearch: AcqSearchService,
+ private perm: PermService
+ ) {
+ this.permissions = {};
+ }
+
+ ngOnInit() {
+ this.gridSource = this.acqSearch.getAcqSearchDataSource('picklist');
+
+ this.perm.hasWorkPermHere(['CREATE_PICKLIST', 'UPDATE_PICKLIST', 'VIEW_PICKLIST']).
+ then(perms => this.permissions = perms);
+
+ this.noSelectedRows = (rows: IdlObject[]) => (rows.length === 0);
+ this.oneSelectedRows = (rows: IdlObject[]) => (rows.length === 1);
+ this.createNotAppropriate = (rows: IdlObject[]) => (!this.permissions.CREATE_PICKLIST);
+ this.cloneNotAppropriate = (rows: IdlObject[]) => (!this.permissions.CREATE_PICKLIST || !this.oneSelectedRows(rows));
+ this.mergeNotAppropriate = (rows: IdlObject[]) => (!this.permissions.UPDATE_PICKLIST || this.noSelectedRows(rows));
+ this.deleteNotAppropriate = (rows: IdlObject[]) => (!this.permissions.UPDATE_PICKLIST || this.noSelectedRows(rows));
+
+ this.cellTextGenerator = {
+ name: row => row.name(),
+ };
+ }
+
+ openCreateDialog() {
+ this.picklistCreateDialog.open().subscribe(
+ modified => {
+ if (!modified) { return; }
+ this.createSelectionListString.current().then(msg => this.toast.success(msg));
+ this.picklistResultsGrid.reload(); // FIXME - spec calls for inserted grid row and not refresh
+ }
+ );
+ this.picklistCreateDialog.update(); // clear and focus the textbox
+ }
+
+ openCloneDialog(rows: IdlObject[]) {
+ this.picklistCloneDialog.open().subscribe(
+ modified => {
+ if (!modified) { return; }
+ this.cloneSelectionListString.current().then(msg => this.toast.success(msg));
+ this.picklistResultsGrid.reload(); // FIXME - spec calls for inserted grid row and not refresh
+ }
+ );
+ this.picklistCloneDialog.update(); // update the dialog UI with selections
+ }
+
+ openDeleteDialog(rows: IdlObject[]) {
+ this.picklistDeleteDialog.open().subscribe(
+ modified => {
+ if (!modified) { return; }
+ this.deleteSelectionListString.current().then(msg => this.toast.success(msg));
+ this.picklistResultsGrid.reload(); // FIXME - spec calls for removed grid rows and not refresh
+ }
+ );
+ this.picklistDeleteDialog.update(); // update the dialog UI with selections
+ }
+
+ openMergeDialog(rows: IdlObject[]) {
+ this.picklistMergeDialog.open().subscribe(
+ modified => {
+ if (!modified) { return; }
+ this.mergeSelectionListString.current().then(msg => this.toast.success(msg));
+ this.picklistResultsGrid.reload(); // FIXME - spec calls for removed grid rows and not refresh
+ }
+ );
+ this.picklistMergeDialog.update(); // update the dialog UI with selections
+ }
+
+ showRow(row: any) {
+ window.open('/eg/staff/acq/legacy/picklist/view/' + row.id(), '_blank');
+ }
+
+ doSearch(search: AcqSearch) {
+ setTimeout(() => {
+ this.acqSearch.setSearch(search);
+ this.picklistResultsGrid.reload();
+ });
+ }
+}
--- /dev/null
+<eg-acq-search-form #acqSearchForm (searchSubmitted)="doSearch($event)" [initialSearchTerms]="initialSearchTerms"
+ i18n-searchTypeLabel searchTypeLabel="Purchase Order" runImmediatelySetting="eg.acq.search.purchaseorders.run_immediately"
+ [fallbackSearchTerms]="fallbackSearchTerms"
+ defaultSearchSetting="eg.acq.search.default.purchaseorders"></eg-acq-search-form>
+
+<ng-template #nameTmpl let-purchaseorder="row">
+ <a href="/eg/staff/acq/legacy/po/view/{{purchaseorder.id()}}"
+ target="_blank">
+ {{purchaseorder.name()}}
+ </a>
+</ng-template>
+
+<ng-template #providerTmpl let-purchaseorder="row">
+ <a href="/eg/staff/admin/acq/conify/provider/{{purchaseorder.provider().id()}}"
+ target="_blank">
+ {{purchaseorder.provider().code()}}
+ </a>
+</ng-template>
+
+<eg-grid #acqSearchPurchaseOrdersGrid
+ persistKey="acq.search.purchaseorders"
+ [stickyHeader]="true"
+ [filterable]="true"
+ [sortable]="true"
+ [cellTextGenerator]="cellTextGenerator"
+ (onRowActivate)="showRow($event)"
+ idlClass="acqpo" [dataSource]="gridSource">
+
+ <eg-grid-column path="name" [cellTemplate]="nameTmpl"></eg-grid-column>
+ <eg-grid-column path="id"></eg-grid-column>
+ <eg-grid-column path="provider" [asyncSupportsEmptyTermClick]="true" [cellTemplate]="providerTmpl"></eg-grid-column>
+ <eg-grid-column path="ordering_agency"></eg-grid-column>
+ <eg-grid-column path="create_time"></eg-grid-column>
+ <eg-grid-column path="edit_time"></eg-grid-column>
+ <eg-grid-column path="order_date"></eg-grid-column>
+
+ <eg-grid-column path="creator" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path="editor" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path="owner" [hidden]="true"></eg-grid-column>
+ <eg-grid-column [asyncSupportsEmptyTermClick]="true" i18n-label label="Status" path="state" [disableTooltip]="true"></eg-grid-column>
+ <eg-grid-column [asyncSupportsEmptyTermClick]="true" path="cancel_reason"></eg-grid-column>
+ <eg-grid-column path="prepayment_required" [sortable]="false"></eg-grid-column>
+
+</eg-grid>
--- /dev/null
+import {Component, OnInit, Input, ViewChild} 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 {AcqSearchService, AcqSearchTerm, AcqSearch} from './acq-search.service';
+import {AcqSearchFormComponent} from './acq-search-form.component';
+
+@Component({
+ selector: 'eg-purchase-order-results',
+ templateUrl: 'purchase-order-results.component.html',
+ providers: [AcqSearchService]
+})
+export class PurchaseOrderResultsComponent implements OnInit {
+
+ @Input() initialSearchTerms: AcqSearchTerm[] = [];
+
+ gridSource: GridDataSource;
+ @ViewChild('acqSearchForm', { static: true}) acqSearchForm: AcqSearchFormComponent;
+ @ViewChild('acqSearchPurchaseOrdersGrid', { static: true }) purchaseOrderResultsGrid: GridComponent;
+
+ cellTextGenerator: GridCellTextGenerator;
+
+ fallbackSearchTerms: AcqSearchTerm[] = [{
+ field: 'acqpo:ordering_agency',
+ op: '',
+ value1: this.auth.user() ? this.auth.user().ws_ou() : '',
+ value2: ''
+ }, {
+ field: 'acqpo:state',
+ op: '',
+ value1: 'on-order',
+ value2: ''
+ }];
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private net: NetService,
+ private auth: AuthService,
+ private acqSearch: AcqSearchService) {
+ }
+
+ ngOnInit() {
+ this.gridSource = this.acqSearch.getAcqSearchDataSource('purchase_order');
+
+ this.cellTextGenerator = {
+ provider: row => row.provider().code(),
+ name: row => row.name(),
+ };
+ }
+
+ showRow(row: any) {
+ window.open('/eg/staff/acq/legacy/po/view/' + row.id(), '_blank');
+ }
+
+ doSearch(search: AcqSearch) {
+ setTimeout(() => {
+ this.acqSearch.setSearch(search);
+ this.purchaseOrderResultsGrid.reload();
+ });
+ }
+}
--- /dev/null
+import {Injectable} from '@angular/core';
+import {Router, Resolve, RouterStateSnapshot,
+ ActivatedRouteSnapshot} from '@angular/router';
+import {AttrDefsService} from './attr-defs.service';
+
+@Injectable()
+export class AttrDefsResolver implements Resolve<Promise<any[]>> {
+
+ savedId: number = null;
+
+ constructor(
+ private router: Router,
+ private attrDefs: AttrDefsService,
+ ) {}
+
+ resolve(
+ route: ActivatedRouteSnapshot,
+ state: RouterStateSnapshot): Promise<any[]> {
+
+ return Promise.all([
+ this.attrDefs.fetchAttrDefs()
+ ]);
+ }
+
+}
--- /dev/null
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {AcqSearchComponent} from './acq-search.component';
+import {AttrDefsResolver} from './resolver.service';
+import {AttrDefsService} from './attr-defs.service';
+
+const routes: Routes = [
+ { path: '',
+ component: AcqSearchComponent,
+ resolve: { attrDefsResolver : AttrDefsResolver },
+ runGuardsAndResolvers: 'always'
+ },
+ { path: ':searchtype',
+ component: AcqSearchComponent,
+ resolve: { attrDefsResolver : AttrDefsResolver },
+ runGuardsAndResolvers: 'always'
+ }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule],
+ providers: [AttrDefsResolver, AttrDefsService]
+})
+
+export class AcqSearchRoutingModule {}
</a>
<div class="dropdown-menu" ngbDropdownMenu>
<a class="dropdown-item"
- href="/eg/staff/acq/legacy/search/unified">
+ routerLink="/staff/acq/search">
<span class="material-icons" aria-hidden="true">search</span>
<span i18n>General Search</span>
</a>
<div class="dropdown-divider"></div>
- <a class="dropdown-item"
- href="/eg/staff/acq/legacy/search/unified?ca=pl">
+ <a class="dropdown-item"
+ routerLink="/staff/acq/search/selectionlists">
<span class="material-icons" aria-hidden="true">view_list</span>
- <span i18n>My Selection Lists</span>
+ <span i18n>Selection Lists</span>
</a>
<a class="dropdown-item"
href="/eg/staff/acq/legacy/picklist/brief_record">
<span class="material-icons" aria-hidden="true">cloud_upload</span>
<span i18n>Load MARC Order Records</span>
</a>
- <a class="dropdown-item"
- href="/eg/staff/acq/legacy/search/unified?ca=po">
+ <a class="dropdown-item"
+ routerLink="/staff/acq/search/purchaseorders">
<span class="material-icons" aria-hidden="true">shopping_cart</span>
<span i18n>Purchase Orders</span>
</a>
<span class="material-icons" aria-hidden="true">contact_phone</span>
<span i18n>Claim-Ready Items</span>
</a>
- <a class="dropdown-item"
- href="/eg/staff/acq/legacy/search/unified?ca=inv">
+ <a class="dropdown-item"
+ routerLink="/staff/acq/search/invoices">
<span class="material-icons" aria-hidden="true">attach_money</span>
- <span i18n>Open Invoices</span>
+ <span i18n>Invoices</span>
</a>
<a class="dropdown-item"
href="/eg/staff/acq/legacy/invoice/view?create=1">
return this.auth.user() ? this.auth.user().usrname() : '';
}
+ user_id() {
+ return this.auth.user() ? this.auth.user().id() : '';
+ }
+
workstation() {
return this.auth.user() ? this.auth.workstation() : '';
}
+ ws_ou() {
+ return this.auth.user() ? this.auth.user().ws_ou() : '';
+ }
+
setLocale(locale: any) {
this.locale.setLocale(locale.code());
}
redirectTo: 'splash',
pathMatch: 'full',
}, {
+ path: 'acq',
+ loadChildren: () =>
+ import('@eg/staff/acq/routing.module').then(m => m.AcqRoutingModule)
+ }, {
path: 'booking',
loadChildren: () =>
import('./booking/booking.module').then(m => m.BookingModule)
#staff-content-container {
width: 95%;
margin-top:56px;
+ margin-bottom: 50px;
padding-right: 10px;
padding-left: 10px;
margin-right: auto;
@media (min-width: 1600px) { .modal-xl { max-width: 1500px; } }
@media (min-width: 1700px) { .modal-xl { max-width: 1600px; } }
+/**
+ * Make the acquisitions search form's navigation tabs match
+ * those of the staff interface. This is a global rule because
+ * various approaches to doing it local to the acq search component
+ * don't work:
+ *
+ * 1. A rule bound to the container of the acq search tabset
+ * would make the entire background be grey.
+ * 2. ":host ::ng-deep" for a local rule works, but depends on a
+ * mechanism that is deprecated.
+ * 4. ng-tabset provides no hooks for custom styles for the nav-tab
+ * background.
+ * 5. Turning off view encapsulation for the acq search component
+ * breaks a lot of styles.
+ */
+#acq-search-page ngb-tabset .nav.nav-tabs {
+ background-color: rgb(247, 247, 247);
+}
</a>
<ul uib-dropdown-menu>
<li>
- <a href="./acq/legacy/search/unified" target="_self">
+ <a href="/eg2/staff/acq/search" target="_self">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
[% l('General Search') %]
</a>
</li>
<li class="divider"></li>
<li>
- <a href="./acq/legacy/search/unified?ca=pl" target="_self">
+ <a href="/eg2/staff/acq/search/selectionlists" target="_self">
<span class="glyphicon glyphicon-list" aria-hidden="true"></span>
- [% l('My Selection Lists') %]
+ [% l('Selection Lists') %]
</a>
</li>
<li>
</a>
</li>
<li>
- <a href="./acq/legacy/search/unified?ca=po" target="_self">
+ <a href="/eg2/staff/acq/search/purchaseorders" target="_self">
<span class="glyphicon glyphicon-shopping-cart" aria-hidden="true"></span>
[% l('Purchase Orders') %]
</a>
</a>
</li>
<li>
- <a href="./acq/legacy/search/unified?ca=inv" target="_self">
+ <a href="/eg2/staff/acq/search/invoices" target="_self">
<span class="glyphicon glyphicon-usd" aria-hidden="true"></span>
- [% l('Open Invoices') %]
+ [% l('Invoices') %]
</a>
</li>
<li>
if (egCore.auth.user()) {
$scope.op_changed = egCore.auth.OCtoken() ? true : false;
$scope.username = egCore.auth.user().usrname();
+ $scope.user_id = egCore.auth.user().id();
+ $scope.ws_ou = egCore.auth.user().ws_ou();
$scope.workstation = egCore.auth.workstation();
egCore.org.settings([