--- /dev/null
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+
+const routes: Routes = [
+ { path: 'search',
+ loadChildren: './search/acq-search.module#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
+<div id="acq-search-form" class="pl-3 pr-3 pt-3 pb-3">
+<form>
+ <div class="row mb-1">
+ <div class="col-lg-5 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 i18n select value="all">all</option>
+ <option i18n select value="any">any</option>
+ </select>
+ of the following terms:</label>
+ </div>
+ <div class="col-lg-6"></div>
+ <div class="col-lg-1">
+ <a class="with-material-icon no-href text-primary"
+ title="Show Form" i18n-title
+ *ngIf="!showForm" (click)="showForm=true">
+ <span class="material-icons">expand_more</span>
+ </a>
+ <a class="with-material-icon no-href text-primary"
+ title="Hide Form" i18n-title
+ *ngIf="showForm" (click)="showForm=false">
+ <span class="material-icons">expand_less</span>
+ </a>
+ </div>
+ </div>
+ <div class="row mb-1" *ngFor="let t of searchTerms; let idx=index" [hidden]="!showForm">
+ <div class="col-lg-3">
+ <select class="form-control" id="selected-search-term" [ngModelOptions]="{standalone: true}" [(ngModel)]="t.field"
+ (change)="clearSearchTerm(t)">
+ <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-lg-2">
+ <select class="form-control" id="selected-search-op" [ngModelOptions]="{standalone: true}" [(ngModel)]="t.op"
+ (change)="clearSearchTermValueAfterOpChange(t)">
+ <option i18n value="">is</option>
+ <option i18n value="__not">is NOT</option>
+ <option i18n value="__fuzzy" [disabled]="searchTermDatatypes[t.field] != 'text' && searchFieldLinkedClasses[t.field] !== 'acqpro'">contains</option>
+ <option i18n value="__not,__fuzzy" [disabled]="searchTermDatatypes[t.field] != 'text'">does NOT contain</option>
+ <option i18n value="__starts" [disabled]="searchTermDatatypes[t.field] != 'text'">STARTS with</option>
+ <option i18n value="__ends" [disabled]="searchTermDatatypes[t.field] != 'text'">ENDS with</option>
+ <option i18n value="__lte" [disabled]="searchTermDatatypes[t.field] != 'timestamp'">is on or BEFORE</option>
+ <option i18n value="__gte" [disabled]="searchTermDatatypes[t.field] != 'timestamp'">is on or AFTER</option>
+ <option i18n value="__between" [disabled]="searchTermDatatypes[t.field] != 'timestamp'">is BETWEEN</option>
+ <option i18n value="__age" [disabled]="searchTermDatatypes[t.field] != 'timestamp'">age (relative date)</option>
+ <option i18n value="__in">matches a term from a file</option>
+ </select>
+ </div>
+ <div class="col-lg-3">
+ <ng-container *ngIf="t.op == '__in'">
+ <eg-file-reader [(ngModel)]="t.value1" [ngModelOptions]="{standalone: true}"></eg-file-reader>
+ </ng-container>
+ <ng-container *ngIf="t.op !== '__in'">
+ <div *ngIf="t.field.endsWith(':state') && (t.op === '' || t.op === '__not'); else notStateField">
+ <select class="form-control" [ngModelOptions]="{standalone: true}" [(ngModel)]="t.value1">
+ <option i18n value="new">New</option>
+ <option i18n *ngIf="!t.field.startsWith('acqpo')" value="selector-ready">Selector-Ready</option>
+ <option i18n *ngIf="!t.field.startsWith('acqpo')" value="order-ready">Order-Ready</option>
+ <option i18n *ngIf="!t.field.startsWith('acqpo')" value="approved">Approved</option>
+ <option i18n *ngIf="t.field.startsWith('acqpo')" value="pending">Pending</option>
+ <option i18n *ngIf="!t.field.startsWith('acqpo')" value="pending-order">Pending-Order</option>
+ <option i18n value="on-order">On-Order</option>
+ <option i18n value="received">Received</option>
+ <option i18n value="cancelled">Cancelled</option>
+ </select>
+ </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" />
+ <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'"
+ [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'" class="form-control" />
+ </ng-container>
+ <ng-container *ngIf="searchFieldLinkedClasses[t.field] !== 'acqpro'">
+ <eg-combobox
+ [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'"
+ (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
+ (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-lg-2 pl-0 pr-1">
+ <button class="btn btn-sm material-icon-button" type="button"
+ (click)="addSearchTerm()"
+ i18n-title title="Add Search Row">
+ <span class="material-icons">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="material-icons">remove_circle_outline</span>
+ </button>
+ </div>
+ </div>
+ <div class="row" [hidden]="!showForm">
+ <div class="col-lg-2">
+ <button class="form-control btn btn-success" (click)="submitSearch()" type="submit" i18n>Search</button>
+ </div>
+ <div class="col-lg-5"></div>
+ <div class="col-lg-2">
+ <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 class="col-lg-3">
+ <button class="form-control btn btn-primary" (click)="saveSearchAsDefault()" type="button" i18n>Set As Default {{searchTypeLabel}} Search</button>
+ </div>
+ </div>
+</form>
+</div>
--- /dev/null
+import {Component, OnInit, AfterViewInit, Input, Output, EventEmitter} 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 {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 {
+
+ @Input() initialSearchTerms: AcqSearchTerm[] = [];
+ @Input() defaultSearchSetting = '';
+ @Input() runImmediatelySetting = '';
+ @Input() searchTypeLabel = '';
+
+ @Output() searchSubmitted = new EventEmitter<AcqSearch>();
+
+ showForm = true;
+
+ hints = ['jub', 'acqpl', 'acqpo', 'acqinv', 'acqlid'];
+ availableSearchFields = {};
+ searchTermDatatypes = {};
+ searchFieldLinkedClasses = {};
+ validSearchTypes = ['lineitems', 'purchaseorders', 'invoices', 'selectionlists'];
+ defaultSearchType = 'lineitems';
+ searchConjunction = 'all';
+ runImmediately = false;
+
+ searchTerms: AcqSearchTerm[] = [];
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private pcrud: PcrudService,
+ private store: ServerStoreService,
+ private idl: IdlService,
+ ) {}
+
+ 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;
+ 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 (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;
+ } else {
+ this.addSearchTerm();
+ }
+ if (this.runImmediately) {
+ this.submitSearch();
+ }
+ }
+ );
+ }
+ });
+ }
+
+ ngAfterViewInit() {}
+
+ addSearchTerm() {
+ this.searchTerms.push({ field: '', op: '', value1: '', value2: '' });
+ }
+ delSearchTerm(index: number) {
+ if (this.searchTerms.length < 2) {
+ this.clearSearchTerm(this.searchTerms[0]);
+ } else {
+ this.searchTerms.splice(index, 1);
+ }
+ }
+ clearSearchTerm(term: AcqSearchTerm) {
+ term.value1 = '';
+ term.value2 = '';
+ term.is_date = false;
+
+ 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) {
+ if (term.op === '__age') {
+ 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
+ });
+ }
+ 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">
+ <ngb-tabset #acqSearchTabs [activeId]="searchType" (tabChange)="onTabChange($event)">
+ <ngb-tab title="Line Items Search" i18n-title id="lineitems">
+ <ng-template ngbTabContent><eg-lineitem-results [initialSearchTerms]="urlSearchTerms"></eg-lineitem-results></ng-template>
+ </ngb-tab>
+ <ngb-tab title="Purchase Orders Search" i18n-title id="purchaseorders">
+ <ng-template ngbTabContent><eg-purchase-order-results [initialSearchTerms]="urlSearchTerms"></eg-purchase-order-results></ng-template>
+ </ngb-tab>
+ <ngb-tab title="Invoices Search" i18n-title id="invoices">
+ <ng-template ngbTabContent><eg-invoice-results [initialSearchTerms]="urlSearchTerms"></eg-invoice-results></ng-template>
+ </ngb-tab>
+ <ngb-tab title="Selection Lists Search" i18n-title id="selectionlists">
+ <ng-template ngbTabContent><eg-picklist-results [initialSearchTerms]="urlSearchTerms"></eg-picklist-results></ng-template>
+ </ngb-tab>
+ </ngb-tabset>
+ </div>
+</div>
--- /dev/null
+import {Component, OnInit, AfterViewInit, ViewChild, ViewChildren, QueryList} from '@angular/core';
+import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+import {Router, ActivatedRoute, ParamMap, 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 {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 {
+
+ searchType = '';
+ validSearchTypes = ['lineitems', 'purchaseorders', 'invoices', 'selectionlists'];
+ defaultSearchType = 'lineitems';
+
+ urlSearchTerms: AcqSearchTerm[] = [];
+
+ onTabChange: ($event: NgbTabChangeEvent) => void;
+ @ViewChild('acqSearchTabs', { static: true }) tabs: NgbTabset;
+ @ViewChildren(LineitemResultsComponent) liResults: QueryList<PurchaseOrderResultsComponent>;
+ @ViewChildren(PurchaseOrderResultsComponent) poResults: QueryList<PurchaseOrderResultsComponent>;
+ @ViewChildren(InvoiceResultsComponent) invResults: QueryList<PurchaseOrderResultsComponent>;
+ @ViewChildren(PicklistResultsComponent) plResults: QueryList<PurchaseOrderResultsComponent>;
+
+ 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.subscribe(routeEvent => {
+ if (routeEvent instanceof NavigationEnd) {
+ 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]);
+ }
+ }
+
+ this.onTabChange = ($event) => {
+ if (this.validSearchTypes.includes($event.nextId)) {
+ this.searchType = $event.nextId;
+ this.urlSearchTerms = [];
+ this.router.navigate(['/staff', 'acq', 'search', $event.nextId]);
+ }
+ };
+ }
+
+ ngAfterViewInit() {}
+
+}
--- /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';
+
+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,
+ },
+ purchase_order: {
+ no_flesh_cancel_reason: true,
+ flesh_provider: true,
+ flesh_owner: false,
+ flesh_creator: false,
+ flesh_editor: false
+ },
+ picklist: {
+ flesh_lineitem_count: true,
+ flesh_owner: true,
+ flesh_creator: false,
+ flesh_editor: false
+ },
+ invoice: {
+ no_flesh_misc: true,
+ flesh_provider: true // and shipper, which is also a provider
+ }
+};
+
+const operatorMap = {
+ '!=': '__not',
+ '>': '__gte',
+ '>=': '__gte',
+ '<=': '__lte',
+ '<': '__lte',
+ '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';
+ attrDefs: {[code: string]: IdlObject};
+ firstRun = true;
+
+ constructor(
+ private net: NetService,
+ private evt: EventService,
+ private auth: AuthService,
+ private pcrud: PcrudService
+ ) {
+ this.attrDefs = {};
+ this.firstRun = true;
+ }
+
+ 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();
+ });
+ });
+ }
+
+ 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 === '') {
+ return;
+ }
+ const searchTerm: Object = {};
+ const recType = term.field.split(':')[0];
+ const searchField = term.field.split(':')[1];
+ if (term.op === '__between') {
+ searchTerm[searchField] = [term.value1, term.value2];
+ } else {
+ searchTerm[searchField] = term.value1;
+ }
+ if (term.op !== '') {
+ 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)) {
+ if (!('acqlia' in andTerms)) {
+ andTerms['acqlia'] = [];
+ }
+ searchTerm[this.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();
+
+ this.fetchAttrDefs().then(() => {
+ gridSource.getRows = (pager: Pager) => {
+
+ // 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;
+ 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
+<eg-acq-search-form (searchSubmitted)="doSearch($event)" [initialSearchTerms]="initialSearchTerms"
+ i18n-searchTypeLabel searchTypeLabel="Invoice" runImmediatelySetting="eg.acq.search.invoices.run_immediately"
+ 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"
+ 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>
+
+<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} 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('acqSearchInvoicesGrid', { static: true }) invoiceResultsGrid: GridComponent;
+ @ViewChild('printfail', { static: true }) private printfail: AlertDialogComponent;
+
+ noSelectedRows: (rows: IdlObject[]) => boolean;
+
+ 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);
+ }
+
+ 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'
+ })
+ );
+ }
+
+ doSearch(search: AcqSearch) {
+ setTimeout(() => {
+ this.acqSearch.setSearch(search);
+ this.invoiceResultsGrid.reload();
+ });
+ }
+}
--- /dev/null
+<eg-acq-search-form (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()}}?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()}}?focus_li={{lineitem.id()}}"
+ target="_blank">
+ {{lineitem.id()}}
+ </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().name()}}
+ </a>
+</ng-template>
+
+<ng-template #liLinksTmpl let-lineitem="row">
+ <ul>
+ <li *ngIf="lineitem.eg_bib_id()">
+ <a href="/eg/staff/cat/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()}}"
+ 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()}}"
+ target="_blank" i18n>Selection List</a></li>
+ </ul>
+</ng-template>
+
+<eg-grid #acqSearchLineitemsGrid
+ persistKey="acq.search.lineitems"
+ idlClass="jub" [dataSource]="gridSource"
+ [stickyHeader]="true"
+ [filterable]="true"
+ [showDeclaredFieldsOnly]="true">
+
+ <eg-grid-column path="id" [cellTemplate]="idTmpl" [disableTooltip]="true"></eg-grid-column>
+ <!-- TODO: Title and Author filters will require special work as they're acqlia values -->
+ <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"></eg-grid-column>
+ <eg-grid-column path="claim_policy.name"></eg-grid-column>
+ <eg-grid-column 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>
--- /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} 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('acqSearchLineitemsGrid', { static: true }) lineitemResultsGrid: GridComponent;
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private net: NetService,
+ private auth: AuthService,
+ private acqSearch: AcqSearchService) {
+ }
+
+ ngOnInit() {
+ this.gridSource = this.acqSearch.getAcqSearchDataSource('lineitem');
+ }
+
+ doSearch(search: AcqSearch) {
+ setTimeout(() => {
+ this.acqSearch.setSearch(search);
+ this.lineitemResultsGrid.reload();
+ });
+ }
+}
--- /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();
+ } else {
+ console.log(res);
+ }
+ },
+ (err) => {
+ console.error(err);
+ this.fail.open();
+ },
+ () => 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>
--- /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;
+
+ 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);
+ this.fail.open();
+ } else {
+ console.log(res);
+ }
+ },
+ (err) => {
+ console.error(err);
+ this.fail.open();
+ },
+ () => 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();
+ } else {
+ console.log(res);
+ }
+ },
+ (err) => {
+ console.error(err);
+ this.fail.open();
+ },
+ () => 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 p !== that.leadList; })
+ ).subscribe(
+ (res) => {
+ if (this.evt.parse(res)) {
+ console.error(res);
+ this.fail.open();
+ } else {
+ console.log(res);
+ }
+ },
+ (err) => {
+ console.error(err);
+ this.fail.open();
+ },
+ () => this.close(true)
+ );
+ }
+
+}
+
+
--- /dev/null
+<eg-acq-search-form (searchSubmitted)="doSearch($event)" [initialSearchTerms]="initialSearchTerms"
+ i18n-searchTypeLabel searchTypeLabel="Selection List" runImmediatelySetting="eg.acq.search.selectionlists.run_immediately"
+ 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"
+ idlClass="acqpl" [dataSource]="gridSource">
+
+ <eg-grid-toolbar-action label="New Selection List" i18n-label
+ (onClick)="openCreateDialog()" [disableOnRows]="createNotAppropriate">
+ </eg-grid-toolbar-action>
+ <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"></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} 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('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;
+
+ 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));
+ }
+
+ openCreateDialog() {
+ this.picklistCreateDialog.open().subscribe(
+ modified => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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
+ }
+
+ doSearch(search: AcqSearch) {
+ setTimeout(() => {
+ this.acqSearch.setSearch(search);
+ this.picklistResultsGrid.reload();
+ });
+ }
+}
--- /dev/null
+<eg-acq-search-form (searchSubmitted)="doSearch($event)" [initialSearchTerms]="initialSearchTerms"
+ i18n-searchTypeLabel searchTypeLabel="Purchase Order" runImmediatelySetting="eg.acq.search.purchaseorders.run_immediately"
+ 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"
+ idlClass="acqpo" [dataSource]="gridSource">
+
+ <eg-grid-column path="name" [cellTemplate]="nameTmpl"></eg-grid-column>
+ <eg-grid-column path="provider" [cellTemplate]="providerTmpl"></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>
--- /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} 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('acqSearchPurchaseOrdersGrid', { static: true }) purchaseOrderResultsGrid: GridComponent;
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private net: NetService,
+ private auth: AuthService,
+ private acqSearch: AcqSearchService) {
+ }
+
+ ngOnInit() {
+ this.gridSource = this.acqSearch.getAcqSearchDataSource('purchase_order');
+ }
+
+ doSearch(search: AcqSearch) {
+ setTimeout(() => {
+ this.acqSearch.setSearch(search);
+ this.purchaseOrderResultsGrid.reload();
+ });
+ }
+}
--- /dev/null
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {AcqSearchComponent} from './acq-search.component';
+
+const routes: Routes = [
+ { path: '',
+ component: AcqSearchComponent
+ },
+ { path: ':searchtype',
+ component: AcqSearchComponent
+ }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule],
+ providers: []
+})
+
+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">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">
+ routerLink="/staff/acq/search/selectionlists"
+ [queryParams]="{f: 'acqpl:owner', val1: user_id()}">
<span class="material-icons">view_list</span>
<span i18n>My Selection Lists</span>
</a>
<span i18n>Load MARC Order Records</span>
</a>
<a class="dropdown-item"
- href="/eg/staff/acq/legacy/search/unified?ca=po">
+ routerLink="/staff/acq/search/purchaseorders"
+ [queryParams]="{f: ['acqpo:ordering_agency','acqpo:state'], val1: [ws_ou(), 'on-order']}">
<span class="material-icons">shopping_cart</span>
<span i18n>Purchase Orders</span>
</a>
<span i18n>Claim-Ready Items</span>
</a>
<a class="dropdown-item"
- href="/eg/staff/acq/legacy/search/unified?ca=inv">
+ routerLink="/staff/acq/search/invoices"
+ [queryParams]="{f: ['acqinv:receiver', 'acqinv:close_date'], val1: [ws_ou(), null]}">
<span class="material-icons">attach_money</span>
<span i18n>Open Invoices</span>
</a>
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 : '@eg/staff/acq/routing.module#AcqRoutingModule'
+ }, {
path: 'booking',
loadChildren : '@eg/staff/booking/booking.module#BookingModule'
}, {
background-color: #c9efe4;
color: black;
}
+
+/**
+ * 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"></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?f=acqpl:owner&val1={{user_id}}" target="_self">
<span class="glyphicon glyphicon-list"></span>
[% l('My Selection Lists') %]
</a>
</a>
</li>
<li>
- <a href="./acq/legacy/search/unified?ca=po" target="_self">
+ <a href="/eg2/staff/acq/search/purchaseorders?f=acqpo:ordering_agency&f=acqpo:state&val1={{ws_ou}}&val1=on-order" target="_self">
<span class="glyphicon glyphicon-shopping-cart"></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?f=acqinv:receiver&f=acqinv:close_date&val1={{ws_ou}}&val1=null" target="_self">
<span class="glyphicon glyphicon-usd"></span>
[% l('Open Invoices') %]
</a>
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([