From d1ccf1fb231f8221ce90732efdb7dda646a2637b Mon Sep 17 00:00:00 2001 From: Galen Charlton Date: Wed, 3 Mar 2021 18:25:40 -0500 Subject: [PATCH] LP#1904244: Angular funds interface Signed-off-by: Galen Charlton Signed-off-by: Ruth Frasur Signed-off-by: Bill Erickson --- .../admin/acq/admin-acq-splash.component.html | 13 +- .../funds/fund-details-dialog.component.html | 209 +++++++++++++ .../funds/fund-details-dialog.component.ts | 296 ++++++++++++++++++ .../funds/fund-rollover-dialog.component.html | 87 +++++ .../funds/fund-rollover-dialog.component.ts | 146 +++++++++ .../admin/acq/funds/fund-tags.component.html | 34 ++ .../admin/acq/funds/fund-tags.component.ts | 124 ++++++++ .../funds/fund-transfer-dialog.component.html | 50 +++ .../funds/fund-transfer-dialog.component.ts | 113 +++++++ ...-source-transactions-dialog.component.html | 83 +++++ ...ng-source-transactions-dialog.component.ts | 169 ++++++++++ .../acq/funds/funding-sources.component.html | 118 +++++++ .../acq/funds/funding-sources.component.ts | 234 ++++++++++++++ .../acq/funds/funds-manager.component.html | 117 +++++++ .../acq/funds/funds-manager.component.ts | 177 +++++++++++ .../admin/acq/funds/funds.component.html | 33 ++ .../staff/admin/acq/funds/funds.component.ts | 52 +++ .../app/staff/admin/acq/funds/funds.module.ts | 37 +++ .../staff/admin/acq/funds/routing.module.ts | 18 ++ .../src/app/staff/admin/acq/routing.module.ts | 31 ++ 20 files changed, 2130 insertions(+), 11 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-details-dialog.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-details-dialog.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-rollover-dialog.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-rollover-dialog.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-tags.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-tags.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-transfer-dialog.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-transfer-dialog.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funding-source-transactions-dialog.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funding-source-transactions-dialog.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funding-sources.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funding-sources.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds-manager.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds-manager.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds.module.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/acq/funds/routing.module.ts diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq-splash.component.html b/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq-splash.component.html index 3efdf6ce35..b2fec57c0d 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq-splash.component.html +++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq-splash.component.html @@ -27,17 +27,8 @@ --> - - - - - - + + + + +
+ +
+
+ + {{value}} + +
+
+ + + + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-details-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-details-dialog.component.ts new file mode 100644 index 0000000000..662e9f1053 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-details-dialog.component.ts @@ -0,0 +1,296 @@ +import {Component, Input, ViewChild, TemplateRef, OnInit} from '@angular/core'; +import {DialogComponent} from '@eg/share/dialog/dialog.component'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {FormatService} from '@eg/core/format.service'; +import {EventService} from '@eg/core/event.service'; +import {NetService} from '@eg/core/net.service'; +import {AuthService} from '@eg/core/auth.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {StoreService} from '@eg/core/store.service'; +import {OrgService} from '@eg/core/org.service'; +import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; +import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid'; +import {Pager} from '@eg/share/util/pager'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {FundTagsComponent} from './fund-tags.component'; +import {FundTransferDialogComponent} from './fund-transfer-dialog.component'; +import {map, mergeMap} from 'rxjs/operators'; +import {Observable, forkJoin, of} from 'rxjs'; + +@Component({ + selector: 'eg-fund-details-dialog', + templateUrl: './fund-details-dialog.component.html' +}) + +export class FundDetailsDialogComponent + extends DialogComponent implements OnInit { + + @Input() fundId: number; + fund: IdlObject; + idlDef: any; + fieldOrder: any; + acqfaDataSource: GridDataSource; + acqftrDataSource: GridDataSource; + acqfdebDataSource: GridDataSource; + + @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent; + @ViewChild('transferDialog', { static: false }) transferDialog: FundTransferDialogComponent; + @ViewChild('allocateToFundDialog', { static: true }) allocateToFundDialog: FmRecordEditorComponent; + @ViewChild('successString', { static: true }) successString: StringComponent; + @ViewChild('updateFailedString', { static: false }) updateFailedString: StringComponent; + + activeTab = 'summary'; + defaultTabType = 'summary'; + cellTextGenerator: GridCellTextGenerator; + + constructor( + private idl: IdlService, + private evt: EventService, + private net: NetService, + private auth: AuthService, + private pcrud: PcrudService, + private store: StoreService, + private org: OrgService, + private format: FormatService, + private toast: ToastService, + private modal: NgbModal + ) { + super(modal); + } + + ngOnInit() { + + this.defaultTabType = + this.store.getLocalItem('eg.acq.fund_details.default_tab') || 'summary'; + this.activeTab = this.defaultTabType; + + this.fund = null; + this.onOpen$.subscribe(() => { + this.activeTab = this.defaultTabType; + this._initRecord(); + }); + this.idlDef = this.idl.classes['acqf']; + this.fieldOrder = 'name,code,year,org,active,currency_type,balance_stop_percentage,balance_warning_percentage,propagate,rollover'; + + this.cellTextGenerator = { + src_fund: row => + row.src_fund().code() + ' (' + + row.src_fund().year() + ') (' + + this.getOrgShortname(row.src_fund().org()) + ')', + dest_fund: row => + row.dest_fund().code() + ' (' + + row.dest_fund().year() + ') (' + + this.getOrgShortname(row.dest_fund().org()) + ')', + funding_source: row => + row.funding_source().code() + ' (' + + this.getOrgShortname(row.funding_source().owner()) + ')', + funding_source_credit: row => + row.funding_source_credit().funding_source().code() + ' (' + + this.getOrgShortname(row.funding_source_credit().funding_source().owner()) + ')', + li: row => row.li_id, + po: row => row.po_name, + invoice: row => row.vendor_invoice_id + }; + } + + private _initRecord() { + this.fund = null; + this.acqfaDataSource = this._getDataSource('acqfa', 'create_time ASC'); + this.acqftrDataSource = this._getDataSource('acqftr', 'transfer_time ASC'); + this.acqfdebDataSource = this._getDataSource('acqfdeb', 'create_time ASC'); + this.pcrud.retrieve('acqf', this.fundId, { + flesh: 1, + flesh_fields: { + acqf: [ + 'spent_balance', + 'combined_balance', + 'spent_total', + 'encumbrance_total', + 'debit_total', + 'allocation_total', + 'org', + 'currency_type' + ] + } + }).subscribe(res => this.fund = res); + } + + _getDataSource(idlClass: string, sortField: string): GridDataSource { + const gridSource = new GridDataSource(); + + gridSource.getRows = (pager: Pager, sort: any[]) => { + const orderBy: any = {}; + if (sort.length) { + // Sort specified from grid + orderBy[idlClass] = sort[0].name + ' ' + sort[0].dir; + } else if (sortField) { + // Default sort field + orderBy[idlClass] = sortField; + } + + const searchOps = { + offset: pager.offset, + limit: pager.limit, + order_by: orderBy, + }; + const reqOps = { + fleshSelectors: true, + }; + + const search: any = new Array(); + if (idlClass === 'acqfa') { + search.push({ fund: this.fundId }); + } else if (idlClass === 'acqftr') { + search.push({ + '-or': [ + { src_fund: this.fundId }, + { dest_fund: this.fundId } + ] + }); + searchOps['flesh'] = 2; + searchOps['flesh_fields'] = { + 'acqftr': ['funding_source_credit'], + 'acqfscred': ['funding_source'] + }; + } else if (idlClass === 'acqfdeb') { + search.push({ fund: this.fundId }); + searchOps['flesh'] = 3; + searchOps['flesh_fields'] = { + 'acqfdeb': ['invoice_entry', 'invoice_items', 'po_items', 'lineitem_details'], + 'acqie': ['invoice', 'purchase_order', 'lineitem'], + 'acqii': ['invoice'], + 'acqpoi': ['purchase_order'], + 'acqlid': ['lineitem'], + 'jub': ['purchase_order'] + }; + } + + Object.keys(gridSource.filters).forEach(key => { + Object.keys(gridSource.filters[key]).forEach(key2 => { + search.push(gridSource.filters[key][key2]); + }); + }); + + return this.pcrud.search(idlClass, search, searchOps, reqOps) + .pipe(mergeMap((row) => this.doExtraFleshing(row))); + }; + + return gridSource; + } + + doExtraFleshing(row: IdlObject): Observable { + if (row.classname === 'acqfdeb') { + row['vendor_invoice_id'] = null; + row['invoice_id'] = null; + row['po_id'] = null; + row['po_name'] = null; + row['li_id'] = null; + // TODO need to verify this, but we may be able to get away with + // the assumption that a given fund debit never has more than + // one line item, purchase order, or invoice associated with it. + if (row.invoice_entry()) { + if (row.invoice_entry().invoice()) { + row['invoice_id'] = row.invoice_entry().invoice().id(); + row['vendor_invoice_id'] = row.invoice_entry().invoice().inv_ident(); + } + if (row.invoice_entry().purchase_order()) { + row['po_id'] = row.invoice_entry().purchase_order().id(); + row['po_name'] = row.invoice_entry().purchase_order().name(); + } + if (row.invoice_entry().lineitem()) { + row['li_id'] = row.invoice_entry().lineitem().id(); + } + } + if (row.lineitem_details() && row.lineitem_details().length) { + if (row.lineitem_details()[0].lineitem().purchase_order()) { + row['li_id'] = row.lineitem_details()[0].lineitem().id(); + row['po_id'] = row.lineitem_details()[0].lineitem().purchase_order().id(); + row['po_name'] = row.lineitem_details()[0].lineitem().purchase_order().name(); + } + } + if (row.po_items() && row.po_items().length) { + if (row.po_items()[0].purchase_order()) { + row['po_id'] = row.po_items()[0].purchase_order().id(); + row['po_name'] = row.po_items()[0].purchase_order().name(); + } + } + if (row.invoice_items() && row.invoice_items().length) { + if (row.invoice_items()[0].invoice()) { + row['invoice_id'] = row.invoice_items()[0].invoice().id(); + row['vendor_invoice_id'] = row.invoice_items()[0].invoice().inv_ident(); + } + } + } + return of(row); + } + formatCurrency(value: any) { + return this.format.transform({ + value: value, + datatype: 'money' + }); + } + + openEditDialog() { + this.editDialog.recordId = this.fundId; + this.editDialog.mode = 'update'; + this.editDialog.open({size: 'lg'}).subscribe( + result => { + this.successString.current() + .then(str => this.toast.success(str)); + this._initRecord(); + }, + error => { + this.updateFailedString.current() + .then(str => this.toast.danger(str)); + } + ); + } + + allocateToFund() { + const allocation = this.idl.create('acqfa'); + allocation.fund(this.fundId); + allocation.allocator(this.auth.user().id()); + this.allocateToFundDialog.defaultNewRecord = allocation; + this.allocateToFundDialog.mode = 'create'; + + this.allocateToFundDialog.hiddenFieldsList = ['id', 'fund', 'allocator', 'create_time']; + this.allocateToFundDialog.fieldOrder = 'funding_source,amount,note'; + this.allocateToFundDialog.open().subscribe( + result => { + this.successString.current() + .then(str => this.toast.success(str)); + this._initRecord(); + }, + error => { + this.updateFailedString.current() + .then(str => this.toast.danger(str)); + } + ); + } + + doTransfer() { + this.transferDialog.sourceFund = this.fund; + this.transferDialog.open().subscribe( + ok => this._initRecord() + ); + } + + setDefaultTab() { + this.defaultTabType = this.activeTab; + this.store.setLocalItem('eg.acq.fund_details.default_tab', this.activeTab); + } + + getOrgShortname(ou: any) { + if (typeof ou === 'object') { + return ou.shortname(); + } else { + return this.org.get(ou).shortname(); + } + } + + checkNegativeAmount(val: any) { + return Number(val) < 0; + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-rollover-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-rollover-dialog.component.html new file mode 100644 index 0000000000..8734f55adb --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-rollover-dialog.component.html @@ -0,0 +1,87 @@ + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-rollover-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-rollover-dialog.component.ts new file mode 100644 index 0000000000..c7f995d8a2 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-rollover-dialog.component.ts @@ -0,0 +1,146 @@ +import {Component, Input, ViewChild, TemplateRef, OnInit} from '@angular/core'; +import {DialogComponent} from '@eg/share/dialog/dialog.component'; +import {NgForm} from '@angular/forms'; +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 {PcrudService} from '@eg/core/pcrud.service'; +import {GridDataSource} from '@eg/share/grid/grid'; +import {Pager} from '@eg/share/util/pager'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {PermService} from '@eg/core/perm.service'; +import {OrgService} from '@eg/core/org.service'; +import {Observable} from 'rxjs'; +import {map} from 'rxjs/operators'; +import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component'; +import {ComboboxEntry} from '@eg/share/combobox/combobox.component'; + +@Component({ + selector: 'eg-fund-rollover-dialog', + templateUrl: './fund-rollover-dialog.component.html' +}) + +export class FundRolloverDialogComponent + extends DialogComponent implements OnInit { + + doneLoading = false; + + @Input() contextOrgId: number; + + @ViewChild('successString', { static: true }) successString: StringComponent; + @ViewChild('updateFailedString', { static: false }) updateFailedString: StringComponent; + @ViewChild('rolloverProgress', { static: true }) + private rolloverProgress: ProgressInlineComponent; + + includeDescendants = false; + doCloseout = false; + showEncumbOnly = false; + limitToEncumbrances = false; + dryRun = true; + contextOrg: IdlObject; + isProcessing = false; + showResults = false; + years: ComboboxEntry[]; + year: number; + + count: number; + amount_rolled: number; + + constructor( + private idl: IdlService, + private evt: EventService, + private net: NetService, + private auth: AuthService, + private pcrud: PcrudService, + private perm: PermService, + private toast: ToastService, + private org: OrgService, + private modal: NgbModal + ) { + super(modal); + } + + ngOnInit() { + this.onOpen$.subscribe(() => this._initDialog()); + this.doneLoading = true; + } + + private _initDialog() { + this.contextOrg = this.org.get(this.contextOrgId); + this.includeDescendants = false; + this.doCloseout = false; + this.limitToEncumbrances = false; + this.showResults = false; + this.dryRun = true; + this.years = null; + this.year = null; + let maxYear = 0; + this.net.request( + 'open-ils.acq', + 'open-ils.acq.fund.org.years.retrieve', + this.auth.token(), + {}, + { limit_perm: 'VIEW_FUND' } + ).subscribe( + years => { + this.years = years.map(y => { + if (maxYear < y) { maxYear = y; } + return { id: y, label: y }; + }); + this.year = maxYear; + } + ); + this.showEncumbOnly = false; + this.org.settings('acq.fund.allow_rollover_without_money', this.contextOrgId).then((ous) => { + this.showEncumbOnly = ous['acq.fund.allow_rollover_without_money']; + }); + } + + rollover() { + this.isProcessing = true; + + const rolloverResponses: any = []; + + let method = 'open-ils.acq.fiscal_rollover'; + if (this.doCloseout) { + method += '.combined'; + } else { + method += '.propagate'; + } + if (this.dryRun) { method += '.dry_run'; } + + this.count = 0; + this.amount_rolled = 0; + + this.net.request( + 'open-ils.acq', + method, + this.auth.token(), + this.year, + this.contextOrgId, + this.includeDescendants, + { encumb_only : this.limitToEncumbrances } + ).subscribe( + r => { + rolloverResponses.push(r.fund); + this.count++; + this.amount_rolled += Number(r.rollover_amount); + }, + err => {}, + () => { + this.isProcessing = false; + this.showResults = true; + if (!this.dryRun) { + this.successString.current() + .then(str => this.toast.success(str)); + } + // note that we're intentionally not closing the dialog + // so that user can view the results + } + ); + } + +} diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-tags.component.html b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-tags.component.html new file mode 100644 index 0000000000..2188a368d6 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-tags.component.html @@ -0,0 +1,34 @@ +Added tag + +Failed to add tag + +Removed tag + +Failed to remove tag + + +
+
+ + {{ftm.tag().name()}} ({{ftm.tag().owner().shortname()}}) +
+
+
+
+ +
+
+ +
+
+ (tag is already assigned to this fund) +
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-tags.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-tags.component.ts new file mode 100644 index 0000000000..aba54fc129 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-tags.component.ts @@ -0,0 +1,124 @@ +import {Component, OnInit, Input, ViewChild} from '@angular/core'; +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 {PcrudService} from '@eg/core/pcrud.service'; +import {OrgService} from '@eg/core/org.service'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {ComboboxComponent} from '@eg/share/combobox/combobox.component'; +import {ComboboxEntry} from '@eg/share/combobox/combobox.component'; +import {Observable} from 'rxjs'; +import {map} from 'rxjs/operators'; + +@Component({ + selector: 'eg-fund-tags', + templateUrl: './fund-tags.component.html' +}) +export class FundTagsComponent implements OnInit { + + @Input() fundId: number; + @Input() fundOwner: number; + + @ViewChild('addSuccessString', { static: true }) addSuccessString: StringComponent; + @ViewChild('addErrorString', { static: true }) addErrorString: StringComponent; + @ViewChild('removeSuccessString', { static: true }) removeSuccessString: StringComponent; + @ViewChild('removeErrorString', { static: true }) removeErrorString: StringComponent; + @ViewChild('tagSelector', { static: false }) tagSelector: ComboboxComponent; + + tagMaps: IdlObject[]; + newTag: ComboboxEntry = null; + tagSelectorDataSource: (term: string) => Observable; + + constructor( + private idl: IdlService, + private evt: EventService, + private net: NetService, + private auth: AuthService, + private pcrud: PcrudService, + private org: OrgService, + private toast: ToastService + ) {} + + ngOnInit() { + this._loadTagMaps(); + this.tagSelectorDataSource = term => { + const field = 'name'; + const args = {}; + const extra_args = { order_by : {} }; + args[field] = {'ilike': `%${term}%`}; // could -or search on label + args['owner'] = this.org.ancestors(this.fundOwner, true); + extra_args['order_by']['acqft'] = field; + extra_args['limit'] = 100; + extra_args['flesh'] = 2; + const flesh_fields: Object = {}; + flesh_fields['acqft'] = ['owner']; + extra_args['flesh_fields'] = flesh_fields; + return this.pcrud.search('acqft', args, extra_args).pipe(map(data => { + return { + id: data.id(), + label: data.name() + ' (' + data.owner().shortname() + ')', + fm: data + }; + })); + }; + } + + _loadTagMaps() { + this.tagMaps = []; + this.pcrud.search('acqftm', { fund: this.fundId }, { + flesh: 2, + flesh_fields: { + acqftm: ['tag'], + acqft: ['owner'] + } + }).subscribe( + res => this.tagMaps.push(res), + err => {}, + () => this.tagMaps.sort((a, b) => { + return a.tag().name() < b.tag().name() ? -1 : 1; + }) + ); + } + + checkNewTagAlreadyMapped(): boolean { + if ( this.newTag == null) { return false; } + const matches: IdlObject[] = this.tagMaps.filter(tm => tm.tag().id() === this.newTag.id); + return matches.length > 0 ? true : false; + } + + addTagMap() { + const ftm = this.idl.create('acqftm'); + ftm.tag(this.newTag.id); + ftm.fund(this.fundId); + this.pcrud.create(ftm).subscribe( + ok => { + this.addSuccessString.current() + .then(str => this.toast.success(str)); + }, + err => { + this.addErrorString.current() + .then(str => this.toast.danger(str)); + }, + () => { + this.newTag = null; + this.tagSelector.selectedId = null; + this._loadTagMaps(); + } + ); + } + removeTagMap(ftm: IdlObject) { + this.pcrud.remove(ftm).subscribe( + ok => { + this.removeSuccessString.current() + .then(str => this.toast.success(str)); + }, + err => { + this.removeErrorString.current() + .then(str => this.toast.danger(str)); + }, + () => this._loadTagMaps() + ); + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-transfer-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-transfer-dialog.component.html new file mode 100644 index 0000000000..fda39763ed --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-transfer-dialog.component.html @@ -0,0 +1,50 @@ + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-transfer-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-transfer-dialog.component.ts new file mode 100644 index 0000000000..8b45fc4109 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-transfer-dialog.component.ts @@ -0,0 +1,113 @@ +import {Component, Input, ViewChild, TemplateRef, OnInit} from '@angular/core'; +import {DialogComponent} from '@eg/share/dialog/dialog.component'; +import {NgForm} from '@angular/forms'; +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 {PcrudService} from '@eg/core/pcrud.service'; +import {GridDataSource} from '@eg/share/grid/grid'; +import {Pager} from '@eg/share/util/pager'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {PermService} from '@eg/core/perm.service'; +import {ComboboxComponent} from '@eg/share/combobox/combobox.component'; +import {ComboboxEntry} from '@eg/share/combobox/combobox.component'; +import {Observable} from 'rxjs'; +import {map} from 'rxjs/operators'; + +@Component({ + selector: 'eg-fund-transfer-dialog', + templateUrl: './fund-transfer-dialog.component.html' +}) + +export class FundTransferDialogComponent + extends DialogComponent implements OnInit { + + @Input() sourceFund: IdlObject; + doneLoading = false; + + @ViewChild('successString', { static: true }) successString: StringComponent; + @ViewChild('updateFailedString', { static: false }) updateFailedString: StringComponent; + @ViewChild('fundSelector', { static: false }) tagSelector: ComboboxComponent; + + fundDataSource: (term: string) => Observable; + destFund: ComboboxEntry = null; + sourceAmount = null; + note = null; + + constructor( + private idl: IdlService, + private evt: EventService, + private net: NetService, + private auth: AuthService, + private pcrud: PcrudService, + private perm: PermService, + private toast: ToastService, + private modal: NgbModal + ) { + super(modal); + } + + ngOnInit() { + this.destFund = null; + this.onOpen$.subscribe(() => this._initRecord()); + this.fundDataSource = term => { + const field = 'code'; + const args = {}; + const extra_args = { order_by : {} }; + args[field] = {'ilike': `%${term}%`}; // could -or search on label + args['active'] = 't'; + extra_args['order_by']['acqf'] = field; + extra_args['limit'] = 100; + extra_args['flesh'] = 1; + const flesh_fields: Object = {}; + flesh_fields['acqf'] = ['org']; + extra_args['flesh_fields'] = flesh_fields; + return this.pcrud.search('acqf', args, extra_args).pipe(map(data => { + return { + id: data.id(), + label: data.code() + + ' (' + data.year() + ')' + + ' (' + data.org().shortname() + ')', + fm: data + }; + })); + }; + } + + private _initRecord() { + this.doneLoading = false; + this.destFund = { id: null }; // destFund is a ComoboxEntry, so + // we need to clear it like this + this.sourceAmount = null; + this.note = null; + this.doneLoading = true; + } + + transfer() { + this.net.request( + 'open-ils.acq', + 'open-ils.acq.funds.transfer_money', + this.auth.token(), + this.sourceFund.id(), + this.sourceAmount, + this.destFund.id, + null, + this.note + ).subscribe( + res => { + this.successString.current() + .then(str => this.toast.success(str)); + this.close(true); + }, + res => { + this.updateFailedString.current() + .then(str => this.toast.danger(str)); + this.close(false); + } + ); + } + +} diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funding-source-transactions-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funding-source-transactions-dialog.component.html new file mode 100644 index 0000000000..100c8def17 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funding-source-transactions-dialog.component.html @@ -0,0 +1,83 @@ + + + + +
+ +
+
+ + {{value}} + +
+
+ + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funding-source-transactions-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funding-source-transactions-dialog.component.ts new file mode 100644 index 0000000000..2d052b356f --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funding-source-transactions-dialog.component.ts @@ -0,0 +1,169 @@ +import {Component, Input, ViewChild, TemplateRef, OnInit} from '@angular/core'; +import {DialogComponent} from '@eg/share/dialog/dialog.component'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {FormatService} from '@eg/core/format.service'; +import {EventService} from '@eg/core/event.service'; +import {NetService} from '@eg/core/net.service'; +import {AuthService} from '@eg/core/auth.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; +import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid'; +import {Pager} from '@eg/share/util/pager'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {OrgService} from '@eg/core/org.service'; + +@Component({ + selector: 'eg-funding-source-transactions-dialog', + templateUrl: './funding-source-transactions-dialog.component.html' +}) + +export class FundingSourceTransactionsDialogComponent + extends DialogComponent implements OnInit { + + @Input() fundingSourceId: number; + @Input() activeTab = 'credits'; + fundingSource: IdlObject; + idlDef: any; + fieldOrder: any; + acqfaDataSource: GridDataSource; + acqfscredDataSource: GridDataSource; + cellTextGenerator: GridCellTextGenerator; + + @ViewChild('applyCreditDialog', { static: true }) applyCreditDialog: FmRecordEditorComponent; + @ViewChild('allocateToFundDialog', { static: true }) allocateToFundDialog: FmRecordEditorComponent; + @ViewChild('successString', { static: true }) successString: StringComponent; + @ViewChild('updateFailedString', { static: false }) updateFailedString: StringComponent; + + constructor( + private idl: IdlService, + private evt: EventService, + private net: NetService, + private auth: AuthService, + private pcrud: PcrudService, + private org: OrgService, + private format: FormatService, + private toast: ToastService, + private modal: NgbModal + ) { + super(modal); + } + + ngOnInit() { + this.cellTextGenerator = { + fund: row => { + return row.code() + ' (' + row.year() + ') (' + + this.getOrgShortname(row.org()) + ')'; + } + }; + this.fundingSource = null; + this.onOpen$.subscribe(() => this._initRecord()); + this.idlDef = this.idl.classes['acqfs']; + this.fieldOrder = 'name,code,year,org,active,currency_type,balance_stop_percentage,balance_warning_percentage,propagate,rollover'; + } + + private _initRecord() { + this.fundingSource = null; + this.acqfaDataSource = this._getDataSource('acqfa', 'create_time DESC'); + this.acqfscredDataSource = this._getDataSource('acqfscred', 'effective_date DESC'); + this.pcrud.retrieve('acqfs', this.fundingSourceId, {} + ).subscribe(res => this.fundingSource = res); + } + + _getDataSource(idlClass: string, sortField: string): GridDataSource { + const gridSource = new GridDataSource(); + + gridSource.getRows = (pager: Pager, sort: any[]) => { + const orderBy: any = {}; + if (sort.length) { + // Sort specified from grid + orderBy[idlClass] = sort[0].name + ' ' + sort[0].dir; + } else if (sortField) { + // Default sort field + orderBy[idlClass] = sortField; + } + + const searchOps = { + offset: pager.offset, + limit: pager.limit, + order_by: orderBy, + }; + const reqOps = { + fleshSelectors: true, + }; + + const search: any = new Array(); + search.push({ funding_source: this.fundingSourceId }); + + Object.keys(gridSource.filters).forEach(key => { + Object.keys(gridSource.filters[key]).forEach(key2 => { + search.push(gridSource.filters[key][key2]); + }); + }); + + return this.pcrud.search( + idlClass, search, searchOps, reqOps); + }; + + return gridSource; + } + + formatCurrency(value: any) { + return this.format.transform({ + value: value, + datatype: 'money' + }); + } + + createCredit(grid: GridComponent) { + const credit = this.idl.create('acqfscred'); + credit.funding_source(this.fundingSourceId); + this.applyCreditDialog.defaultNewRecord = credit; + this.applyCreditDialog.mode = 'create'; + this.applyCreditDialog.hiddenFieldsList = ['id', 'funding_source']; + this.applyCreditDialog.fieldOrder = 'amount,note,effective_date,deadline_date'; + this.applyCreditDialog.open().subscribe( + result => { + this.successString.current() + .then(str => this.toast.success(str)); + grid.reload(); + }, + error => { + this.updateFailedString.current() + .then(str => this.toast.danger(str)); + } + ); + } + + allocateToFund(grid: GridComponent) { + const allocation = this.idl.create('acqfa'); + allocation.funding_source(this.fundingSourceId); + allocation.allocator(this.auth.user().id()); + this.allocateToFundDialog.defaultNewRecord = allocation; + this.allocateToFundDialog.mode = 'create'; + + this.allocateToFundDialog.hiddenFieldsList = ['id', 'funding_source', 'allocator', 'create_time']; + this.allocateToFundDialog.fieldOrder = 'fund,amount,note'; + this.allocateToFundDialog.open().subscribe( + result => { + this.successString.current() + .then(str => this.toast.success(str)); + grid.reload(); + }, + error => { + this.updateFailedString.current() + .then(str => this.toast.danger(str)); + } + ); + } + + getOrgShortname(ou: any) { + if (typeof ou === 'object') { + return ou.shortname(); + } else { + return this.org.get(ou).shortname(); + } + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funding-sources.component.html b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funding-sources.component.html new file mode 100644 index 0000000000..55599eacdd --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funding-sources.component.html @@ -0,0 +1,118 @@ +{{idlClassDef.label}} Update Succeeded + + +Update of {{idlClassDef.label}} failed + + +Delete of {{idlClassDef.label}} failed or was not allowed + + +{{idlClassDef.label}} Successfully Deleted + + +{{idlClassDef.label}} Successfully Created + + +Failed to create new {{idlClassDef.label}} + + + +
+
+ + + + +
+
+
+
+ + + + + + + + + + {{configLinkLabel(row, col)}} + + + + + + + + + + + + + + + + + + + + + {{row.name()}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funding-sources.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funding-sources.component.ts new file mode 100644 index 0000000000..d5d541c327 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funding-sources.component.ts @@ -0,0 +1,234 @@ +import {Component, Input, ViewChild, OnInit, AfterViewInit} from '@angular/core'; +import {Location} from '@angular/common'; +import {FormatService} from '@eg/core/format.service'; +import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {AdminPageComponent} from '@eg/staff/share/admin-page/admin-page.component'; +import {Pager} from '@eg/share/util/pager'; +import {ActivatedRoute} from '@angular/router'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; +import {OrgService} from '@eg/core/org.service'; +import {PermService} from '@eg/core/perm.service'; +import {AuthService} from '@eg/core/auth.service'; +import {NetService} from '@eg/core/net.service'; +import {map, mergeMap} from 'rxjs/operators'; +import {StringComponent} from '@eg/share/string/string.component'; +import {Observable, forkJoin, of} from 'rxjs'; +import {AlertDialogComponent} from '@eg/share/dialog/alert.component'; +import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component'; +import {FundingSourceTransactionsDialogComponent} from './funding-source-transactions-dialog.component'; + +@Component({ + selector: 'eg-funding-sources', + templateUrl: './funding-sources.component.html' +}) + +export class FundingSourcesComponent extends AdminPageComponent implements OnInit, AfterViewInit { + idlClass = 'acqfs'; + classLabel: string; + + @Input() startId: number; + + @ViewChild('grid', { static: true }) grid: GridComponent; + @ViewChild('fundingSourceTransactionsDialog', { static: false }) + fundingSourceTransactionsDialog: FundingSourceTransactionsDialogComponent; + @ViewChild('applyCreditDialog', { static: true }) applyCreditDialog: FmRecordEditorComponent; + @ViewChild('allocateToFundDialog', { static: true }) allocateToFundDialog: FmRecordEditorComponent; + @ViewChild('alertDialog', {static: false}) private alertDialog: AlertDialogComponent; + @ViewChild('confirmDel', { static: true }) confirmDel: ConfirmDialogComponent; + + cellTextGenerator: GridCellTextGenerator; + notOneSelectedRow: (rows: IdlObject[]) => boolean; + + constructor( + route: ActivatedRoute, + ngLocation: Location, + format: FormatService, + idl: IdlService, + org: OrgService, + auth: AuthService, + pcrud: PcrudService, + perm: PermService, + toast: ToastService, + private net: NetService + ) { + super(route, ngLocation, format, idl, org, auth, pcrud, perm, toast); + this.dataSource = new GridDataSource(); + } + + ngOnInit() { + this.cellTextGenerator = { + name: row => row.name() + }; + this.notOneSelectedRow = (rows: IdlObject[]) => (rows.length !== 1); + this.fieldOrder = 'name,code,year,org,active,currency_type,balance_stop_percentage,balance_warning_percentage,propagate,rollover'; + this.defaultNewRecord = this.idl.create('acqfs'); + this.defaultNewRecord.owner(this.auth.user().ws_ou()); + + this.dataSource.getRows = (pager: Pager, sort: any[]) => { + const orderBy: any = {}; + if (sort.length) { + // Sort specified from grid + orderBy[this.idlClass] = sort[0].name + ' ' + sort[0].dir; + } else if (this.sortField) { + // Default sort field + orderBy[this.idlClass] = this.sortField; + } + + const searchOps = { + offset: pager.offset, + limit: pager.limit, + order_by: orderBy, + flesh: 1, + flesh_fields: { + acqfs: ['credits', 'allocations'] + } + }; + const reqOps = { + fleshSelectors: true, + }; + + if (!this.contextOrg && !Object.keys(this.dataSource.filters).length) { + // No org filter -- fetch all rows + return this.pcrud.retrieveAll( + this.idlClass, searchOps, reqOps) + .pipe(mergeMap((row) => this.calculateSummary(row))); + } + + const search: any = new Array(); + const orgFilter: any = {}; + + if (this.orgField && (this.searchOrgs || this.contextOrg)) { + orgFilter[this.orgField] = + this.searchOrgs.orgIds || [this.contextOrg.id()]; + search.push(orgFilter); + } + + Object.keys(this.dataSource.filters).forEach(key => { + Object.keys(this.dataSource.filters[key]).forEach(key2 => { + search.push(this.dataSource.filters[key][key2]); + }); + }); + + return this.pcrud.search( + this.idlClass, search, searchOps, reqOps) + .pipe(mergeMap((row) => this.calculateSummary(row))); + }; + + super.ngOnInit(); + + this.classLabel = this.idlClassDef.label; + this.includeOrgDescendants = true; + } + + ngAfterViewInit() { + if (this.startId) { + this.pcrud.retrieve('acqfs', this.startId).subscribe( + acqfs => this.openTransactionsDialog([acqfs], 'allocations'), + err => {}, + () => this.startId = null + ); + } + } + + calculateSummary(row: IdlObject): Observable { + row['balance'] = 0; + row['total_credits'] = 0; + row['total_allocations'] = 0; + + row.credits().forEach((c) => row['total_credits'] += Number(c.amount())); + row.allocations().forEach((a) => row['total_allocations'] += Number(a.amount())); + row['balance'] = row['total_credits'] - row['total_allocations']; + return of(row); + } + + deleteSelected(rows: IdlObject[]) { + if (rows.length > 0) { + const id = rows[0].id(); + let can = true; + forkJoin([ + this.pcrud.search('acqfa', { funding_source: id }, { limit: 1 }, { atomic: true }), + this.pcrud.search('acqfscred', { funding_source: id }, { limit: 1 }, { atomic: true }), + ]).subscribe( + results => { + results.forEach((res) => { + if (res.length > 0) { + can = false; + } + }); + }, + err => {}, + () => { + if (can) { + this.confirmDel.open().subscribe(confirmed => { + if (!confirmed) { return; } + super.deleteSelected([ rows[0] ]); + }); + } else { + this.alertDialog.open(); + } + } + ); + } + } + + openTransactionsDialog(rows: IdlObject[], tab: string) { + if (rows.length !== 1) { return; } + this.fundingSourceTransactionsDialog.fundingSourceId = rows[0].id(); + this.fundingSourceTransactionsDialog.activeTab = tab; + this.fundingSourceTransactionsDialog.open({size: 'xl'}).subscribe( + res => {}, + err => {}, + () => this.grid.reload() + ); + } + + createCredit(rows: IdlObject[]) { + if (rows.length !== 1) { return; } + const fundingSourceId = rows[0].id(); + const credit = this.idl.create('acqfscred'); + credit.funding_source(fundingSourceId); + this.applyCreditDialog.defaultNewRecord = credit; + this.applyCreditDialog.mode = 'create'; + this.applyCreditDialog.hiddenFieldsList = ['id', 'funding_source']; + this.applyCreditDialog.fieldOrder = 'amount,note,effective_date,deadline_date'; + this.applyCreditDialog.open().subscribe( + result => { + this.successString.current() + .then(str => this.toast.success(str)); + this.grid.reload(); + }, + error => { + this.updateFailedString.current() + .then(str => this.toast.danger(str)); + } + ); + } + + allocateToFund(rows: IdlObject[]) { + if (rows.length !== 1) { return; } + const fundingSourceId = rows[0].id(); + const allocation = this.idl.create('acqfa'); + allocation.funding_source(fundingSourceId); + allocation.allocator(this.auth.user().id()); + this.allocateToFundDialog.defaultNewRecord = allocation; + this.allocateToFundDialog.mode = 'create'; + + this.allocateToFundDialog.hiddenFieldsList = ['id', 'funding_source', 'allocator', 'create_time']; + this.allocateToFundDialog.fieldOrder = 'fund,amount,note'; + this.allocateToFundDialog.open().subscribe( + result => { + this.successString.current() + .then(str => this.toast.success(str)); + this.grid.reload(); + }, + error => { + this.updateFailedString.current() + .then(str => this.toast.danger(str)); + } + ); + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds-manager.component.html b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds-manager.component.html new file mode 100644 index 0000000000..dfc85b5372 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds-manager.component.html @@ -0,0 +1,117 @@ +{{idlClassDef.label}} Update Succeeded + + +Update of {{idlClassDef.label}} failed + + +Delete of {{idlClassDef.label}} failed or was not allowed + + +{{idlClassDef.label}} Successfully Deleted + + +{{idlClassDef.label}} Successfully Created + + +Failed to create new {{idlClassDef.label}} + + + +
+
+ + + + +
+
+
+
+ + + + + + + + + + {{configLinkLabel(row, col)}} + + + + + + + + + + + + + + + + + {{row.name()}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds-manager.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds-manager.component.ts new file mode 100644 index 0000000000..44a7468b2e --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds-manager.component.ts @@ -0,0 +1,177 @@ +import {Component, Input, ViewChild, OnInit, AfterViewInit} from '@angular/core'; +import {Location} from '@angular/common'; +import {FormatService} from '@eg/core/format.service'; +import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {AdminPageComponent} from '@eg/staff/share/admin-page/admin-page.component'; +import {Pager} from '@eg/share/util/pager'; +import {ActivatedRoute} from '@angular/router'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {OrgService} from '@eg/core/org.service'; +import {PermService} from '@eg/core/perm.service'; +import {AuthService} from '@eg/core/auth.service'; +import {NetService} from '@eg/core/net.service'; +import {StringComponent} from '@eg/share/string/string.component'; +import {FundDetailsDialogComponent} from './fund-details-dialog.component'; +import {FundRolloverDialogComponent} from './fund-rollover-dialog.component'; + +@Component({ + selector: 'eg-funds-manager', + templateUrl: './funds-manager.component.html' +}) + +export class FundsManagerComponent extends AdminPageComponent implements OnInit, AfterViewInit { + idlClass = 'acqf'; + classLabel: string; + + @Input() startId: number; + + @ViewChild('fundDetailsDialog', { static: false }) fundDetailsDialog: FundDetailsDialogComponent; + @ViewChild('fundRolloverDialog', { static: false }) fundRolloverDialog: FundRolloverDialogComponent; + @ViewChild('grid', { static: true }) grid: GridComponent; + + cellTextGenerator: GridCellTextGenerator; + canRollover = false; + + constructor( + route: ActivatedRoute, + ngLocation: Location, + format: FormatService, + idl: IdlService, + org: OrgService, + auth: AuthService, + pcrud: PcrudService, + perm: PermService, + private perm2: PermService, // need copy because perm is private to base + // component + toast: ToastService, + private net: NetService + ) { + super(route, ngLocation, format, idl, org, auth, pcrud, perm, toast); + this.dataSource = new GridDataSource(); + } + + ngOnInit() { + this.cellTextGenerator = { + name: row => row.name() + }; + this.checkRolloverPerms(); + this.fieldOrder = 'name,code,year,org,active,currency_type,balance_stop_percentage,balance_warning_percentage,propagate,rollover'; + this.fieldOptions = { + year: { + min: new Date().getFullYear() - 10, + max: new Date().getFullYear() + 10 + } + }; + this.defaultNewRecord = this.idl.create('acqf'); + this.defaultNewRecord.active(true); + this.defaultNewRecord.org(this.auth.user().ws_ou()); + + this.dataSource.getRows = (pager: Pager, sort: any[]) => { + const orderBy: any = {}; + if (sort.length) { + // Sort specified from grid + orderBy[this.idlClass] = sort[0].name + ' ' + sort[0].dir; + } else if (this.sortField) { + // Default sort field + orderBy[this.idlClass] = this.sortField; + } + + const searchOps = { + offset: pager.offset, + limit: pager.limit, + order_by: orderBy, + flesh_fields: { + acqf: [ + 'spent_balance', + 'combined_balance', + 'spent_total', + 'encumbrance_total', + 'debit_total', + 'allocation_total' + ] + } + }; + const reqOps = { + fleshSelectors: true, + }; + + if (!this.contextOrg && !Object.keys(this.dataSource.filters).length) { + // No org filter -- fetch all rows + return this.pcrud.retrieveAll( + this.idlClass, searchOps, reqOps); + } + + const search: any = new Array(); + const orgFilter: any = {}; + + if (this.orgField && (this.searchOrgs || this.contextOrg)) { + orgFilter[this.orgField] = + this.searchOrgs.orgIds || [this.contextOrg.id()]; + search.push(orgFilter); + } + + Object.keys(this.dataSource.filters).forEach(key => { + Object.keys(this.dataSource.filters[key]).forEach(key2 => { + search.push(this.dataSource.filters[key][key2]); + }); + }); + + return this.pcrud.search( + this.idlClass, search, searchOps, reqOps); + }; + + super.ngOnInit(); + + this.classLabel = this.idlClassDef.label; + this.includeOrgDescendants = true; + } + + ngAfterViewInit() { + if (this.startId) { + this.pcrud.retrieve('acqf', this.startId).subscribe( + acqf => this.openFundDetailsDialog([acqf]), + err => {}, + () => this.startId = null + ); + } + } + + checkRolloverPerms() { + this.canRollover = false; + + this.perm2.hasWorkPermAt(['ADMIN_FUND'], true).then(permMap => { + Object.keys(permMap).forEach(key => { + if (permMap[key].length > 0) { + this.canRollover = true; + } + }); + }); + } + + openFundDetailsDialog(rows: IdlObject[]) { + if (rows.length > 0) { + this.fundDetailsDialog.fundId = rows[0].id(); + this.fundDetailsDialog.open({size: 'xl'}).subscribe( + result => this.grid.reload(), + error => this.grid.reload(), + () => this.grid.reload() + ); + } + } + + getDefaultYear(): string { + return new Date().getFullYear().toString(); + } + + doRollover() { + this.fundRolloverDialog.contextOrgId = this.searchOrgs.primaryOrgId; + this.fundRolloverDialog.open({size: 'lg'}).subscribe( + ok => {}, + err => {}, + () => this.grid.reload() + ); + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds.component.html b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds.component.html new file mode 100644 index 0000000000..0e4cea9502 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds.component.html @@ -0,0 +1,33 @@ + + + + + + +
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds.component.ts new file mode 100644 index 0000000000..92b2a703d7 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds.component.ts @@ -0,0 +1,52 @@ +import {Component, OnInit, Input, ViewChild} from '@angular/core'; +import {Router, ActivatedRoute, ParamMap} from '@angular/router'; +import {Location} from '@angular/common'; +import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + templateUrl: './funds.component.html' +}) +export class FundsComponent implements OnInit { + + activeTab: string; + fundId: number; + fundingSourceId: number; + + constructor( + private location: Location, + private router: Router, + private route: ActivatedRoute + ) {} + + ngOnInit() { + this.route.paramMap.subscribe((params: ParamMap) => { + const tab = params.get('tab'); + const id = +params.get('id'); + if (!id || !tab) { return; } + if (tab === 'fund' || tab === 'funding_source') { + this.activeTab = tab; + if (tab === 'fund') { + this.fundId = id; + } else { + this.fundingSourceId = id; + } + } else { + return; + } + }); + } + + // Changing a tab in the UI means clearing the route (e.g., + // if we originally navigated vi funds/fund/:id or + // funds/funding_source/:id + onNavChange(evt: NgbNavChangeEvent) { + // clear any IDs parsed from the original route + // to avoid reopening the fund or funding source + // dialogs when navigating back to the fund/funding source + // tab. + this.fundId = null; + this.fundingSourceId = null; + const url = this.router.createUrlTree(['/staff/admin/acq/funds']).toString(); + this.location.go(url); // go without reloading + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds.module.ts new file mode 100644 index 0000000000..d7f4d043cd --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds.module.ts @@ -0,0 +1,37 @@ +import {NgModule} from '@angular/core'; +import {StaffCommonModule} from '@eg/staff/common.module'; +import {AdminCommonModule} from '@eg/staff/admin/common.module'; +import {FundsComponent} from './funds.component'; +import {FundsRoutingModule} from './routing.module'; +import {FundsManagerComponent} from './funds-manager.component'; +import {FundDetailsDialogComponent} from './fund-details-dialog.component'; +import {FundingSourcesComponent} from './funding-sources.component'; +import {FundingSourceTransactionsDialogComponent} from './funding-source-transactions-dialog.component'; +import {FundTagsComponent} from './fund-tags.component'; +import {FundTransferDialogComponent} from './fund-transfer-dialog.component'; +import {FundRolloverDialogComponent} from './fund-rollover-dialog.component'; + +@NgModule({ + declarations: [ + FundsComponent, + FundsManagerComponent, + FundDetailsDialogComponent, + FundingSourcesComponent, + FundingSourceTransactionsDialogComponent, + FundTagsComponent, + FundTransferDialogComponent, + FundRolloverDialogComponent + ], + imports: [ + StaffCommonModule, + AdminCommonModule, + FundsRoutingModule, + ], + exports: [ + ], + providers: [ + ] +}) + +export class FundsModule { +} diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/routing.module.ts new file mode 100644 index 0000000000..0307e9e3c4 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/routing.module.ts @@ -0,0 +1,18 @@ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {FundsComponent} from './funds.component'; + +const routes: Routes = [{ + path: '', + component: FundsComponent + }, { + path: ':tab/:id', + component: FundsComponent +}]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) + +export class FundsRoutingModule {} diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/acq/routing.module.ts index ff1a05adf2..00d83a4d45 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/acq/routing.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/routing.module.ts @@ -31,6 +31,37 @@ const routes: Routes = [{ }, { path: 'claim_type', redirectTo: 'claiming' // from legacy auto-generated admin page +}, { + path: 'funds', + loadChildren: () => + import('./funds/funds.module').then(m => m.FundsModule) +}, { + path: 'fund', + redirectTo: 'funds' // from auto-generated admin page +}, { + path: 'fund_allocation', + redirectTo: 'funds' // from auto-generated admin page +}, { + path: 'fund_allocation_percent', + redirectTo: 'funds' // from auto-generated admin page +}, { + path: 'fund_debit', + redirectTo: 'funds' // from auto-generated admin page +}, { + path: 'funding_source', + redirectTo: 'funds' // from auto-generated admin page +}, { + path: 'funding_source_credit', + redirectTo: 'funds' // from auto-generated admin page +}, { + path: 'fund_tag', + redirectTo: 'funds' // from auto-generated admin page +}, { + path: 'fund_tag_map', + redirectTo: 'funds' // from auto-generated admin page +}, { + path: 'fund_transfer', + redirectTo: 'funds' // from auto-generated admin page }, { path: ':table', component: BasicAdminPageComponent, -- 2.43.2