-->
<eg-link-table-link i18n-label label="Exchange Rates"
routerLink="/staff/admin/acq/exchange_rate"></eg-link-table-link>
- <eg-link-table-link i18n-label label="Fund Tags"
- routerLink="/staff/admin/acq/fund_tag"></eg-link-table-link>
- <eg-link-table-link i18n-label label="Funding Sources"
- url="/eg/staff/admin/acq/funding_source/list"></eg-link-table-link>
- <!-- TODO
- routerLink="/staff/admin/acq/funding_source"></eg-link-table-link>
- -->
- <!-- TODO fund admin page w/ year filter and rollover -->
- <eg-link-table-link i18n-label label="Funds"
- url="/eg/staff/admin/acq/fund/list"></eg-link-table-link>
- <!-- routerLink="/staff/admin/acq/fund" -->
+ <eg-link-table-link i18n-label label="Fund Administration"
+ routerLink="/staff/admin/acq/funds"></eg-link-table-link>
<eg-link-table-link i18n-label label="Invoice Item Types"
routerLink="/staff/admin/acq/invoice_item_type"></eg-link-table-link>
<eg-link-table-link i18n-label label="Invoice Payment Method"
--- /dev/null
+<eg-string #successString i18n-text text="Fund Update Succeeded"></eg-string>
+<eg-string #updateFailedString i18n-text text="Fund Update Failed"></eg-string>
+
+<ng-template #summaryField let-field="field" let-value="value" let-rawValue="rawValue">
+ <div class="col-2">
+ <label style="font-weight: bold" for="fund-{{field}}">{{idlDef.field_map[field].label}}</label>
+ </div>
+ <div class="col-2">
+ <span id="fund-{{field}}" [ngClass]="{'negative-money-amount': checkNegativeAmount(rawValue)}">
+ {{value}}
+ </span>
+ </div>
+</ng-template>
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <h3 class="modal-title" i18n>Fund Details - {{fund?.name()}} ({{fund?.code()}} ({{fund?.year()}}) ({{fund?.org().shortname()}}))</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">
+ <div class="row mt-3">
+ <div class="col-lg-3">
+ <button class="btn btn-primary"
+ (click)="allocateToFund()" i18n>Create Allocation</button>
+ <button class="btn btn-primary ml-1"
+ (click)="doTransfer()" i18n>Transfer Money</button>
+ </div>
+ <div class="col-lg-9 text-right pb-1">
+ <button class="btn btn-secondary btn-xs" [disabled]="activeTab == defaultTabType"
+ (click)="setDefaultTab()" i18n>Set Default View</button>
+ </div>
+ </div>
+ <ul ngbNav #fundDetailsNav="ngbNav" [(activeId)]="activeTab" class="nav-tabs">
+ <li [ngbNavItem]="'summary'" *ngIf="fund">
+ <a ngbNavLink i18n>Summary</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <div class="row">
+ <button class="btn btn-info ml-3" (click)="openEditDialog()" i18n>Edit Fund</button>
+ </div>
+ <div class="row">
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:fund.code(),field:'code'}"></ng-container>
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:fund.name(),field:'name'}"></ng-container>
+ </div>
+ <div class="row">
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:fund.id(),field:'id'}"></ng-container>
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:fund.year(),field:'year'}"></ng-container>
+ </div>
+ <div class="row">
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:fund.org().shortname(),field:'org'}"></ng-container>
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:fund.currency_type().code(),field:'currency_type'}"></ng-container>
+ </div>
+ <div class="row">
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:formatCurrency(fund.combined_balance()?.amount()),field:'combined_balance',rawValue:fund.combined_balance()?.amount()}"></ng-container>
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:formatCurrency(fund.allocation_total()?.amount()),field:'allocation_total',rawValue:fund.allocation_total()?.amount()}"></ng-container>
+ </div>
+ <div class="row">
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:formatCurrency(fund.spent_balance()?.amount()),field:'spent_balance',rawValue:fund.spent_balance()?.amount()}"></ng-container>
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:formatCurrency(fund.debit_total()?.amount()),field:'debit_total',rawValue:fund.debit_total()?.amount()}"></ng-container>
+ </div>
+ <div class="row">
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:formatCurrency(fund.spent_total()?.amount()),field:'spent_total',rawValue:fund.spent_total()?.amount()}"></ng-container>
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:formatCurrency(fund.encumbrance_total()?.amount()),field:'encumbrance_total',rawValue:fund.encumbrance_total()?.amount()}"></ng-container>
+ </div>
+ </div>
+ </ng-template>
+ </li>
+ <li [ngbNavItem]="'allocations'" *ngIf="fund">
+ <a ngbNavLink i18n>Allocations</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <eg-grid #fundDetailsAllocationsGrid idlClass="acqfa" [dataSource]="acqfaDataSource"
+ [sortable]="true" persistKey="acq.fund.fund_allocation"
+ [cellTextGenerator]="cellTextGenerator"
+ [filterable]="true" [stickyHeader]="true">
+
+ <ng-template #sourceTmpl let-row="row">
+ <a href="/eg2/staff/admin/acq/funds/funding_source/{{row.funding_source().id()}}" target="_blank">
+ {{row.funding_source().code()}} ({{getOrgShortname(row.funding_source().owner())}})
+ </a>
+ </ng-template>
+ <eg-grid-column path="funding_source" [cellTemplate]="sourceTmpl"></eg-grid-column>
+ <eg-grid-column path="amount"></eg-grid-column>
+ <eg-grid-column path="create_time" [datePlusTime]="true"></eg-grid-column>
+ <eg-grid-column path="allocator"></eg-grid-column>
+ <eg-grid-column path="note"></eg-grid-column>
+ <eg-grid-column path="id" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path="fund" [hidden]="true" [filterable]="false"></eg-grid-column>
+ </eg-grid>
+ </div>
+ </ng-template>
+ </li>
+ <li [ngbNavItem]="'transfers'" *ngIf="fund">
+ <a ngbNavLink i18n>Transfers</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <eg-grid #fundDetailsTransfersGrid idlClass="acqftr" [dataSource]="acqftrDataSource"
+ [sortable]="true" persistKey="acq.fund.fund_transfer"
+ [cellTextGenerator]="cellTextGenerator"
+ [filterable]="true" [stickyHeader]="true">
+
+ <ng-template #sourcefundTmpl let-row="row">
+ <ng-container *ngIf="row.src_fund().id() === fundId">
+ {{row.src_fund().code()}} ({{row.src_fund().year()}}) ({{getOrgShortname(row.src_fund().org())}})
+ </ng-container>
+ <ng-container *ngIf="row.src_fund().id() !== fundId">
+ <a href="/eg2/staff/admin/acq/funds/fund/{{row.src_fund().id()}}" target="_blank">
+ {{row.src_fund().code()}} ({{row.src_fund().year()}}) ({{getOrgShortname(row.src_fund().org())}})
+ </a>
+ </ng-container>
+ </ng-template>
+ <eg-grid-column path="src_fund" [cellTemplate]="sourcefundTmpl"></eg-grid-column>
+ <eg-grid-column path="src_amount"></eg-grid-column>
+ <ng-template #destfundTmpl let-row="row">
+ <ng-container *ngIf="row.dest_fund().id() === fundId">
+ {{row.dest_fund().code()}} ({{row.dest_fund().year()}}) ({{getOrgShortname(row.dest_fund().org())}})
+ </ng-container>
+ <ng-container *ngIf="row.dest_fund().id() !== fundId">
+ <a href="/eg2/staff/admin/acq/funds/fund/{{row.dest_fund().id()}}" target="_blank">
+ {{row.dest_fund().code()}} ({{row.dest_fund().year()}}) ({{getOrgShortname(row.dest_fund().org())}})
+ </a>
+ </ng-container>
+ </ng-template>
+ <eg-grid-column path="dest_fund" [cellTemplate]="destfundTmpl"></eg-grid-column>
+ <eg-grid-column path="dest_amount"></eg-grid-column>
+ <eg-grid-column path="transfer_time" [datePlusTime]="true"></eg-grid-column>
+ <eg-grid-column path="transfer_user"></eg-grid-column>
+ <eg-grid-column path="note"></eg-grid-column>
+ <ng-template #fsCredTmpl let-row="row">
+ <a href="/eg2/staff/admin/acq/funds/funding_source/{{row.funding_source_credit().funding_source().id()}}" target="_blank">
+ {{row.funding_source_credit().funding_source().code()}} ({{getOrgShortname(row.funding_source_credit().funding_source().owner())}})
+ </a>
+ </ng-template>
+ <eg-grid-column path="funding_source_credit" [cellTemplate]="fsCredTmpl" i18n-label label="Funding Source"></eg-grid-column>
+ <eg-grid-column path="id" [hidden]="true"></eg-grid-column>
+ </eg-grid>
+ </div>
+ </ng-template>
+ </li>
+ <li [ngbNavItem]="'debits'" *ngIf="fund">
+ <a ngbNavLink i18n>Debits</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <eg-grid #fundDetailsDebitsGrid idlClass="acqfdeb" [dataSource]="acqfdebDataSource"
+ [cellTextGenerator]="cellTextGenerator"
+ [sortable]="true" persistKey="acq.fund.fund_debit"
+ ignoreFields="invoice_entry"
+ [filterable]="true" [stickyHeader]="true">
+
+ <eg-grid-column path="amount"></eg-grid-column>
+ <eg-grid-column path="encumbrance"></eg-grid-column>
+ <eg-grid-column path="debit_type"></eg-grid-column>
+ <eg-grid-column path="origin_amount"></eg-grid-column>
+ <eg-grid-column path="origin_currency_type"></eg-grid-column>
+ <eg-grid-column path="create_time" [datePlusTime]="true"></eg-grid-column>
+ <ng-template #liTmpl let-row="row">
+ <a href="/eg/staff/acq/legacy/po/view/{{row.po_id}}?focus_li={{row.li_id}}" target="_blank">
+ {{row.li_id}}
+ </a>
+ </ng-template>
+ <eg-grid-column path="li" i18n-label label="Line Item" [cellTemplate]="liTmpl" [filterable]="false" [sortable]="false"></eg-grid-column>
+ <ng-template #poTmpl let-row="row">
+ <a href="/eg/staff/acq/legacy/po/view/{{row.po_id}}" target="_blank">
+ {{row.po_name}}
+ </a>
+ </ng-template>
+ <eg-grid-column path="po" i18n-label label="Purchase Order" [cellTemplate]="poTmpl" [filterable]="false" [sortable]="false"></eg-grid-column>
+ <ng-template #invTmpl let-row="row">
+ <a href="/eg/staff/acq/legacy/invoice/view/{{row.invoice_id}}" target="_blank">
+ {{row.vendor_invoice_id}}
+ </a>
+ </ng-template>
+ <eg-grid-column path="invoice" i18n-label label="Invoice" [cellTemplate]="invTmpl" [filterable]="false" [sortable]="false"></eg-grid-column>
+ <eg-grid-column path="po_id" i18n-label label="PO ID" [hidden]="true" [filterable]="false" [sortable]="false"></eg-grid-column>
+ <eg-grid-column path="invoice_id" i18n-label label="Internal Invoice ID" [hidden]="true" [filterable]="false" [sortable]="false"></eg-grid-column>
+ <eg-grid-column path="id" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path="fund" [hidden]="true" [filterable]="false"></eg-grid-column>
+ </eg-grid>
+ </div>
+ </ng-template>
+ </li>
+ <li [ngbNavItem]="'tags'" *ngIf="fund">
+ <a ngbNavLink i18n>Tags</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <eg-fund-tags [fundId]="fundId" [fundOwner]="fund.org()"></eg-fund-tags>
+ </div>
+ </ng-template>
+ </li>
+ </ul>
+ <div [ngbNavOutlet]="fundDetailsNav"></div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-warning"
+ (click)="close()" i18n>Close</button>
+ </div>
+</ng-template>
+
+<eg-fm-record-editor #editDialog idlClass="acqf"
+ [preloadLinkedValues]="true">
+</eg-fm-record-editor>
+
+<eg-fm-record-editor #allocateToFundDialog idlClass="acqfa"
+ [preloadLinkedValues]="true">
+</eg-fm-record-editor>
+
+<eg-fund-transfer-dialog #transferDialog></eg-fund-transfer-dialog>
--- /dev/null
+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<IdlObject> {
+ 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;
+ }
+}
--- /dev/null
+<eg-string #successString i18n-text text="Rollover Succeeded"></eg-string>
+<eg-string #updateFailedString i18n-text text="Rollover Failed"></eg-string>
+
+<ng-template #dialogContent>
+ <div class="modal-header bg-info" *ngIf="doneLoading">
+ <h3 class="modal-title" i18n>Fund Propagation and Rollover</h3>
+ <button type="button" class="close"
+ [disabled]="isProcessing"
+ i18n-aria-label aria-label="Close" (click)="close()">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body" [hidden]="!doneLoading">
+ <form #rolloverForm="ngForm" role="form" class="form-validated">
+ <div class="row col">
+ <label for="year" class="my-1 mr-1" i18n>Year</label>
+ <eg-combobox [entries]="years" *ngIf="years"
+ [required]="true" [selectedId]="year"
+ (onChange)="year = $event ? $event.id : null">
+ </eg-combobox>
+ </div>
+ <div class="row col mt-3" i18n>Context Org Unit: {{contextOrg?.shortname()}}</div>
+ <div class="form-check offset-sm-1 mt-1">
+ <input type="checkbox" name="includeDescendants" id="includeDescendants"
+ class="form-check-input"
+ [(ngModel)]="includeDescendants">
+ <label for="includeDescendants" class="form-check-label" i18n>Include funds from descendant Org Units</label>
+ </div>
+ <hr>
+ <h4 i18n>Fund Propagation</h4>
+ <p i18n>
+ Propagation creates new funds for the next fiscal year. Propagating funds will not affect the money or
+ encumbrances in the funds unless Fiscal Year Close-Out is also done. Only funds that have the Propagate
+ setting enabled will be affected.
+ </p>
+ <p i18n>
+ Funds set to propagate will do so automatically when you click <b>Process</b>.
+ </p>
+ <hr>
+ <h4 i81n>Rollover and Close-Out</h4>
+ <div class="form-check">
+ <input type="checkbox" name="doCloseout" id="doCloseout"
+ class="form-check-input"
+ [(ngModel)]="doCloseout">
+ <label for="doCloseout" class="form-check-label" i18n>Perform Fiscal Year Close-Out</label>
+ <eg-help-popover helpText="Perform Fiscal Year Close-Out deactivates funds for the selected fiscal year. It will also move unspent and encumbered funds to the corresponding fund for the next fiscal year if that fund is configured to Propagate and Rollover." i18n-helpText></eg-help-popover>
+ <span class="alert-warning" *ngIf="doCloseout && !dryRun">Will do a Close-Out for real. If you need to double-check first, check the "Dry Run" checkbox.</span>
+ </div>
+ <div class="offset-sm-1 form-check" *ngIf="showEncumbOnly">
+ <input type="checkbox" name="limitToEncumbrances" id="limitToEncumbrances"
+ class="form-check-input"
+ [(ngModel)]="limitToEncumbrances">
+ <label for="limitToEncumbrances" class="form-check-label" i18n>Limit Fiscal Year Close-Out to Encumbrances</label>
+ <eg-help-popover helpText="This option will limit the Perform Fiscal Year Close-Out procedure to only move encumbrances to the corresponding fund for the next fiscal year. Any unspent money in the funds will not roll over." i18n-helpText></eg-help-popover>
+ </div>
+ <hr>
+ <div class="form-check">
+ <input type="checkbox" name="dryRun" id="dryRun"
+ class="form-check-input"
+ [(ngModel)]="dryRun">
+ <label for="dryRun" class="form-check-label" i18n>Dry Run — no data will be changed</label>
+ <eg-help-popover helpText="Select Dry Run to see a preview of the changes that would occur based on the selected actions. Data will not be changed when Dry Run is selected." i18n-helpText></eg-help-popover>
+ </div>
+ </form>
+ <div class="row" [hidden]="!isProcessing">
+ <div class="col-lg-10 offset-lg-1">
+ <eg-progress-inline #rolloverProgress></eg-progress-inline>
+ </div>
+ </div>
+ <div [hidden]="!showResults" class="row col mt-2">
+ <h4 i18n>Fund Propagation & Rollover Summary for Fiscal Year {{year + 1}}</h4>
+ <ul>
+ <li *ngIf="dryRun" i18n>DRY RUN: these changes have not been committed yet.</li>
+ <li i18n>{{count}} funds propagated for fiscal year {{year + 1}} for the selected locations</li>
+ <li i18n>{{amount_rolled}} unspent money rolled over to fiscal year {{year + 1}} for the selected locations</li>
+ </ul>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-info"
+ [disabled]="isProcessing"
+ (click)="rollover()" i18n>Process</button>
+ <button type="button" class="btn btn-warning"
+ [disabled]="isProcessing"
+ (click)="close()" i18n>Close</button>
+ </div>
+</ng-template>
--- /dev/null
+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
+ }
+ );
+ }
+
+}
--- /dev/null
+<ng-template #addSuccessStrTmpl i18n>Added tag</ng-template>
+<eg-string #addSuccessString [template]="addSuccessStrTmpl"></eg-string>
+<ng-template #addErrorStrTmpl i18n>Failed to add tag</ng-template>
+<eg-string #addErrorString [template]="addErrorStrTmpl"></eg-string>
+<ng-template #removeSuccessStrTmpl i18n>Removed tag</ng-template>
+<eg-string #removeSuccessString [template]="removeSuccessStrTmpl"></eg-string>
+<ng-template #removeErrorStrTmpl i18n>Failed to remove tag</ng-template>
+<eg-string #removeErrorString [template]="removeErrorStrTmpl"></eg-string>
+
+<div class="row">
+ <div class="col-sm-2" *ngFor="let ftm of tagMaps">
+ <button class="btn btn-sm material-icon-button" type="button"
+ (click)="removeTagMap(ftm)"
+ i18n-title title="Remove Tag"><span class="sr-only">Remove Tag</span>
+ <span class="material-icons" aria-hidden="true">delete</span>
+ </button>
+ {{ftm.tag().name()}} ({{ftm.tag().owner().shortname()}})
+ </div>
+</div>
+<div class="row mt-3">
+ <div class="col-sm-2">
+ <eg-combobox #tagSelector [asyncSupportsEmptyTermClick]="true"
+ [(ngModel)]="newTag" [asyncDataSource]="tagSelectorDataSource"
+ i18n-placeholder placeholder="Select tag"></eg-combobox>
+ </div>
+ <div class="col-sm-1">
+ <button class="btn btn-success" [disabled]="!newTag || checkNewTagAlreadyMapped()"
+ (click)="addTagMap()" i18n>Add Tag
+ </button>
+ </div>
+ <div class="col-sm-2" *ngIf="newTag && checkNewTagAlreadyMapped()">
+ <span class="alert-warning" i18n>(tag is already assigned to this fund)</span>
+ </div>
+</div>
--- /dev/null
+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<ComboboxEntry>;
+
+ 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()
+ );
+ }
+}
--- /dev/null
+<eg-string #successString i18n-text text="Fund Transfer Succeeded"></eg-string>
+<eg-string #updateFailedString i18n-text text="Fund Transfer Failed"></eg-string>
+
+<ng-template #dialogContent>
+ <div class="modal-header bg-info" *ngIf="doneLoading">
+ <h3 class="modal-title" i18n>Transfer from Fund {{sourceFund?.name()}} ({{sourceFund?.code()}} ({{sourceFund?.year()}}) ({{sourceFund?.org().shortname()}}))</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" [hidden]="!doneLoading">
+ <form #xfrForm="ngForm" role="form" class="form-validated common-form striped-odd">
+ <div class="form-group row mt-2">
+ <label for="dest-fund" class="col-sm-4 col-form-label" i18n>Destination Fund</label>
+ <div class="col-sm-8">
+ <eg-combobox #fundSelector [asyncSupportsEmptyTermClick]="true"
+ name="dest_fund" id="dest-fund" [mandatory]="true"
+ [(ngModel)]="destFund" [asyncDataSource]="fundDataSource"
+ i18n-placeholder placeholder="Select fund..."></eg-combobox>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label for="source_amount" class="col-sm-4 col-form-label" i18n>Source Amount</label>
+ <div class="col-sm-8">
+ <input class="form-control" type="number" name="source_amount" id="source_amount" [(ngModel)]="sourceAmount" required>
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-sm-4"></div>
+ <div class="col-sm-8" i18n>
+ <i>Amount to transfer from {{sourceFund?.name()}} ({{sourceFund?.code()}} ({{sourceFund?.year()}}) ({{sourceFund?.org().shortname()}}))</i>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label for="note" class="col-sm-4 col-form-label" i18n>Note</label>
+ <div class="col-sm-8">
+ <input class="form-control" type="text" name="note" id="note" [(ngModel)]="note">
+ </div>
+ </div>
+ </form>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-info"
+ [disabled]="!destFund || (sourceAmount <= 0)"
+ (click)="transfer()" i18n>Transfer</button>
+ <button type="button" class="btn btn-warning"
+ (click)="close()" i18n>Close</button>
+ </div>
+</ng-template>
--- /dev/null
+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<ComboboxEntry>;
+ 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);
+ }
+ );
+ }
+
+}
--- /dev/null
+<eg-string #successString i18n-text text="Funding Source Update Succeeded"></eg-string>
+<eg-string #updateFailedString i18n-text text="Funding Source Update Failed"></eg-string>
+
+<ng-template #summaryField let-field="field" let-value="value">
+ <div class="col-2">
+ <label style="font-weight: bold" for="fund-{{field}}">{{idlDef.field_map[field].label}}</label>
+ </div>
+ <div class="col-2">
+ <span id="fund-{{field}}">
+ {{value}}
+ </span>
+ </div>
+</ng-template>
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <h3 class="modal-title" i18n>Funding Source: {{fundingSource?.name()}}</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" *ngIf="fundingSource">
+ <ul ngbNav #fundingSourceTransactionsNav="ngbNav" [(activeId)]="activeTab" class="nav-tabs">
+ <li [ngbNavItem]="'credits'">
+ <a ngbNavLink i18n>Credits</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <eg-grid #creditsGrid idlClass="acqfscred" [dataSource]="acqfscredDataSource"
+ [sortable]="true" persistKey="acq.funding_source.credit"
+ [filterable]="true" [stickyHeader]="true">
+
+ <eg-grid-toolbar-button label="Apply Credits" i18n-label (onClick)="createCredit(creditsGrid)">
+ </eg-grid-toolbar-button>
+ <eg-grid-column path="amount"></eg-grid-column>
+ <eg-grid-column path="effective_date" [datePlusTime]="true"></eg-grid-column>
+ <eg-grid-column path="deadline_date" [datePlusTime]="true"></eg-grid-column>
+ <eg-grid-column path="note"></eg-grid-column>
+ <eg-grid-column path="id" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path="funding_source" [hidden]="true" [filterable]="false"></eg-grid-column>
+ </eg-grid>
+ </div>
+ </ng-template>
+ </li>
+ <li [ngbNavItem]="'allocations'">
+ <a ngbNavLink i18n>Allocations</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <eg-grid #allocationsGrid idlClass="acqfa" [dataSource]="acqfaDataSource"
+ [sortable]="true" persistKey="acq.funding_source.fund_allocation"
+ [filterable]="true" [stickyHeader]="true" [cellTextGenerator]="cellTextGenerator">
+
+ <eg-grid-toolbar-button label="Allocate to Fund" i18n-label (onClick)="allocateToFund(allocationsGrid)">
+ </eg-grid-toolbar-button>
+ <ng-template #fundTmpl let-row="row">
+ <a href="/eg2/staff/admin/acq/funds/fund/{{row.fund().id()}}" target="_blank">
+ {{row.fund().code()}} ({{row.fund().year()}}) ({{getOrgShortname(row.fund().org())}})
+ </a>
+ </ng-template>
+ <eg-grid-column path="fund" [cellTemplate]="fundTmpl"></eg-grid-column>
+ <eg-grid-column path="amount"></eg-grid-column>
+ <eg-grid-column path="create_time" [datePlusTime]="true"></eg-grid-column>
+ <eg-grid-column path="allocator"></eg-grid-column>
+ <eg-grid-column path="note"></eg-grid-column>
+ <eg-grid-column path="id" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path="funding_source" [hidden]="true" [filterable]="false"></eg-grid-column>
+ </eg-grid>
+ </div>
+ </ng-template>
+ </li>
+ </ul>
+ <div [ngbNavOutlet]="fundingSourceTransactionsNav"></div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-warning"
+ (click)="close()" i18n>Close</button>
+ </div>
+</ng-template>
+
+<eg-fm-record-editor #applyCreditDialog idlClass="acqfscred">
+</eg-fm-record-editor>
+<eg-fm-record-editor #allocateToFundDialog idlClass="acqfa"
+ [fieldOptions]="{fund:{linkedSearchConditions:{'active':'t'},preloadLinkedValues:true}}">
+</eg-fm-record-editor>
--- /dev/null
+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();
+ }
+ }
+}
--- /dev/null
+<ng-template #successStrTmpl i18n>{{idlClassDef.label}} Update Succeeded</ng-template>
+<eg-string #successString [template]="successStrTmpl"></eg-string>
+
+<ng-template #updateFailedStrTmpl i18n>Update of {{idlClassDef.label}} failed</ng-template>
+<eg-string #updateFailedString [template]="updateFailedStrTmpl"></eg-string>
+
+<ng-template #deleteFailedStrTmpl i18n>Delete of {{idlClassDef.label}} failed or was not allowed</ng-template>
+<eg-string #deleteFailedString [template]="deleteFailedStrTmpl"></eg-string>
+
+<ng-template #deleteSuccessStrTmpl i18n>{{idlClassDef.label}} Successfully Deleted</ng-template>
+<eg-string #deleteSuccessString [template]="deleteSuccessStrTmpl"></eg-string>
+
+<ng-template #createStrTmpl i18n>{{idlClassDef.label}} Successfully Created</ng-template>
+<eg-string #createString [template]="createStrTmpl"></eg-string>
+
+<ng-template #createErrStrTmpl i18n>Failed to create new {{idlClassDef.label}}</ng-template>
+<eg-string #createErrString [template]="createErrStrTmpl"></eg-string>
+
+<ng-container *ngIf="orgField">
+ <div class="row">
+ <div class="col-lg-6">
+ <ng-container *ngIf="orgField">
+ <eg-org-family-select
+ [limitPerms]="viewPerms"
+ [selectedOrgId]="contextOrg.id()"
+ [(ngModel)]="searchOrgs"
+ (ngModelChange)="grid.reload()">
+ </eg-org-family-select>
+ </ng-container>
+ </div>
+ </div>
+ <hr/>
+</ng-container>
+
+<!-- idlObject and fieldName applied programmatically -->
+<eg-translate #translator></eg-translate>
+
+<ng-container *ngIf="helpTemplate">
+ <ng-container *ngTemplateOutlet="helpTemplate"></ng-container>
+</ng-container>
+
+<ng-template #configFieldLink let-row="row" let-col="col">
+ <a i18n-title title="Link To {{col.label}}"
+ [attr.href]="configFieldLinkUrl(row, col)">{{configLinkLabel(row, col)}}</a>
+</ng-template>
+
+<eg-grid #grid idlClass="{{idlClass}}" [dataSource]="dataSource" hideFields="{{hideGridFields}}"
+ [sortable]="true" persistKey="{{persistKey}}" autoGeneratedColumnOrder="{{fieldOrder}}"
+ [filterable]="true" [stickyHeader]="true"
+ (onRowActivate)="editSelected([$event])"
+ [cellTextGenerator]="cellTextGenerator">
+ <eg-grid-toolbar-button [disabled]="!canCreate"
+ label="New {{idlClassDef.label}}" i18n-label (onClick)="createNew()">
+ </eg-grid-toolbar-button>
+ <eg-grid-toolbar-action label="Edit Selected" i18n-label (onClick)="editSelected($event)"
+ group="Selected" i18n-group [disableOnRows]="notOneSelectedRow">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Delete Selected" i18n-label (onClick)="deleteSelected($event)"
+ group="Selected" i18n-group [disableOnRows]="notOneSelectedRow">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Apply Credit" i18n-label (onClick)="createCredit($event)"
+ group="Credits" i18n-group [disableOnRows]="notOneSelectedRow">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Allocate to Fund" i18n-label (onClick)="allocateToFund($event)"
+ group="Allocations" i18n-group [disableOnRows]="notOneSelectedRow">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="View Credits" i18n-label (onClick)="openTransactionsDialog($event, 'credits')"
+ group="Credits" i18n-groups [disableOnRows]="notOneSelectedRow">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="View Allocations" i18n-label (onClick)="openTransactionsDialog($event, 'allocations')"
+ group="Allocations" i18n-group [disableOnRows]="notOneSelectedRow">
+ </eg-grid-toolbar-action>
+
+ <ng-template #nameTmpl let-row="row">
+ <a href="" (click)="openTransactionsDialog([row], 'credits'); false">
+ {{row.name()}}
+ </a>
+ </ng-template>
+ <eg-grid-column path="name" [cellTemplate]="nameTmpl"></eg-grid-column>
+ <eg-grid-column path="owner"></eg-grid-column>
+ <eg-grid-column path="code"></eg-grid-column>
+ <eg-grid-column path="currency_type"></eg-grid-column>
+ <eg-grid-column path="balance" datatype="money" [filterable]="false" [sortable]="false"
+ i18n-label label="Balance"></eg-grid-column>
+ <eg-grid-column path="total_credits" datatype="money" [filterable]="false" [sortable]="false"
+ i18n-label label="Total Credits"></eg-grid-column>
+ <eg-grid-column path="total_allocations" datatype="money" [filterable]="false" [sortable]="false"
+ i18n-label label="Total Allocations"></eg-grid-column>
+ <eg-grid-column path="id" [hidden]="true"></eg-grid-column>
+
+
+</eg-grid>
+
+<eg-fm-record-editor #editDialog idlClass="{{idlClass}}"
+ [fieldOptions]="fieldOptions"
+ [fieldOrder]="fieldOrder"
+ [defaultNewRecord]="defaultNewRecord"
+ [preloadLinkedValues]="true"
+ [readonlyFields]="readonlyFields">
+</eg-fm-record-editor>
+
+<eg-funding-source-transactions-dialog #fundingSourceTransactionsDialog></eg-funding-source-transactions-dialog>
+
+<eg-confirm-dialog #confirmDel
+ dialogTitle="Delete?" i18n-dialogTitle
+ dialogBody="Delete funding source?" i18n-dialogBody>
+</eg-confirm-dialog>
+<eg-alert-dialog #alertDialog
+ i18n-dialogBody
+ dialogBody="Funding source cannot deleted as it is in use">
+</eg-alert-dialog>
+
+
+<eg-fm-record-editor #applyCreditDialog idlClass="acqfscred">
+</eg-fm-record-editor>
+<eg-fm-record-editor #allocateToFundDialog idlClass="acqfa"
+ [fieldOptions]="{fund:{linkedSearchConditions:{'active':'t'},preloadLinkedValues:true}}">
+</eg-fm-record-editor>
--- /dev/null
+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<IdlObject> {
+ 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));
+ }
+ );
+ }
+}
--- /dev/null
+<ng-template #successStrTmpl i18n>{{idlClassDef.label}} Update Succeeded</ng-template>
+<eg-string #successString [template]="successStrTmpl"></eg-string>
+
+<ng-template #updateFailedStrTmpl i18n>Update of {{idlClassDef.label}} failed</ng-template>
+<eg-string #updateFailedString [template]="updateFailedStrTmpl"></eg-string>
+
+<ng-template #deleteFailedStrTmpl i18n>Delete of {{idlClassDef.label}} failed or was not allowed</ng-template>
+<eg-string #deleteFailedString [template]="deleteFailedStrTmpl"></eg-string>
+
+<ng-template #deleteSuccessStrTmpl i18n>{{idlClassDef.label}} Successfully Deleted</ng-template>
+<eg-string #deleteSuccessString [template]="deleteSuccessStrTmpl"></eg-string>
+
+<ng-template #createStrTmpl i18n>{{idlClassDef.label}} Successfully Created</ng-template>
+<eg-string #createString [template]="createStrTmpl"></eg-string>
+
+<ng-template #createErrStrTmpl i18n>Failed to create new {{idlClassDef.label}}</ng-template>
+<eg-string #createErrString [template]="createErrStrTmpl"></eg-string>
+
+<ng-container *ngIf="orgField">
+ <div class="row">
+ <div class="col-lg-6">
+ <ng-container *ngIf="orgField">
+ <eg-org-family-select
+ [limitPerms]="viewPerms"
+ [selectedOrgId]="contextOrg.id()"
+ [(ngModel)]="searchOrgs"
+ (ngModelChange)="grid.reload()">
+ </eg-org-family-select>
+ </ng-container>
+ </div>
+ </div>
+ <hr/>
+</ng-container>
+
+<!-- idlObject and fieldName applied programmatically -->
+<eg-translate #translator></eg-translate>
+
+<ng-container *ngIf="helpTemplate">
+ <ng-container *ngTemplateOutlet="helpTemplate"></ng-container>
+</ng-container>
+
+<ng-template #configFieldLink let-row="row" let-col="col">
+ <a i18n-title title="Link To {{col.label}}"
+ [attr.href]="configFieldLinkUrl(row, col)">{{configLinkLabel(row, col)}}</a>
+</ng-template>
+
+<eg-grid #grid idlClass="{{idlClass}}" [dataSource]="dataSource" hideFields="{{hideGridFields}}"
+ [sortable]="true" persistKey="{{persistKey}}" autoGeneratedColumnOrder="{{fieldOrder}}"
+ [filterable]="true" [stickyHeader]="true"
+ (onRowActivate)="editSelected([$event])"
+ [cellTextGenerator]="cellTextGenerator">
+ <eg-grid-toolbar-button [disabled]="!canCreate"
+ label="New {{idlClassDef.label}}" i18n-label (onClick)="createNew()">
+ </eg-grid-toolbar-button>
+ <eg-grid-toolbar-button [disabled]="!canRollover"
+ label="Fiscal Propagation and Rollover" i18n-label (onClick)="doRollover()">
+ </eg-grid-toolbar-button>
+ <eg-grid-toolbar-action label="View Selected" i18n-label
+ (onClick)="openFundDetailsDialog($event)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Edit Selected" i18n-label (onClick)="editSelected($event)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Delete Selected" i18n-label (onClick)="deleteSelected($event)">
+ </eg-grid-toolbar-action>
+
+ <ng-template #nameTmpl let-row="row">
+ <a href="" (click)="openFundDetailsDialog([row]); false">
+ {{row.name()}}
+ </a>
+ </ng-template>
+ <eg-grid-column path="name" [cellTemplate]="nameTmpl"></eg-grid-column>
+ <eg-grid-column path="code"></eg-grid-column>
+ <eg-grid-column path="year" initialFilterOperator="=" [initialFilterValue]="getDefaultYear()"></eg-grid-column>
+ <eg-grid-column path="org"></eg-grid-column>
+ <eg-grid-column path="currency_type"></eg-grid-column>
+ <eg-grid-column path="active"></eg-grid-column>
+ <eg-grid-column path="balance_stop_percent"></eg-grid-column>
+ <eg-grid-column path="balance_warning_percent"></eg-grid-column>
+ <eg-grid-column path="propagate"></eg-grid-column>
+ <eg-grid-column path="rollover"></eg-grid-column>
+ <eg-grid-column path="combined_balance.amount"
+ [sortable]="false" [filterable]="false"
+ label="Combined Balance" i18n-label></eg-grid-column>
+ <eg-grid-column path="spent_balance.amount"
+ [sortable]="false" [filterable]="false" [hidden]="true"
+ label="Spent Balance" i18n-label></eg-grid-column>
+ <eg-grid-column path="spent_total.amount"
+ [sortable]="false" [filterable]="false" [hidden]="true"
+ label="Total Spent" i18n-label></eg-grid-column>
+ <eg-grid-column path="encumbrance_total.amount"
+ [sortable]="false" [filterable]="false" [hidden]="true"
+ label="Total Encumbered" i18n-label></eg-grid-column>
+ <eg-grid-column path="debit_total.amount"
+ [sortable]="false" [filterable]="false" [hidden]="true"
+ label="Total Debits" i18n-label></eg-grid-column>
+ <eg-grid-column path="allocation_total.amount"
+ [sortable]="false" [filterable]="false" [hidden]="true"
+ label="Total Allocated" i18n-label></eg-grid-column>
+ <eg-grid-column path="id" [hidden]="true"></eg-grid-column>
+
+
+</eg-grid>
+
+<div class="row mt-5">
+ <small><a href="/eg/staff/admin/acq/fund/list" target="_blank" i18n>Legacy funds interface</a></small>
+</div>
+
+<eg-fm-record-editor #editDialog idlClass="{{idlClass}}"
+ [fieldOptions]="fieldOptions"
+ [fieldOrder]="fieldOrder"
+ [defaultNewRecord]="defaultNewRecord"
+ [preloadLinkedValues]="true"
+ [readonlyFields]="readonlyFields">
+</eg-fm-record-editor>
+
+<eg-fund-details-dialog #fundDetailsDialog></eg-fund-details-dialog>
+<eg-fund-rollover-dialog #fundRolloverDialog></eg-fund-rollover-dialog>
--- /dev/null
+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()
+ );
+ }
+}
--- /dev/null
+<eg-staff-banner bannerText="Fund Administration" i18n-bannerText>
+</eg-staff-banner>
+
+<eg-title i18n-prefix prefix="Fund Administration"></eg-title>
+
+<ul ngbNav #claimingAdminNav="ngbNav" class="nav-tabs" [(activeId)]="activeTab"
+ (navChange)="onNavChange($event)">
+ <li [ngbNavItem]="'fund'">
+ <a ngbNavLink i18n>Funds</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <eg-funds-manager [startId]="fundId"></eg-funds-manager>
+ </div>
+ </ng-template>
+ </li>
+ <li [ngbNavItem]="'funding_source'">
+ <a ngbNavLink i18n>Funding Sources</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <eg-funding-sources [startId]="fundingSourceId"></eg-funding-sources>
+ </div>
+ </ng-template>
+ </li>
+ <li [ngbNavItem]="'fund_tag'">
+ <a ngbNavLink i18n>Fund Tags</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <eg-admin-page idlClass="acqft"></eg-admin-page>
+ </div>
+ </ng-template>
+ </li>
+</ul>
+<div [ngbNavOutlet]="claimingAdminNav"></div>
--- /dev/null
+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
+ }
+}
--- /dev/null
+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 {
+}
--- /dev/null
+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 {}
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,
// All ACQ admin pages cover data in the acq.* schema. No need to