currencies: finish up
authorGalen Charlton <gmc@equinoxinitiative.org>
Sat, 27 Mar 2021 18:45:12 +0000 (14:45 -0400)
committerGalen Charlton <gmc@equinoxinitiative.org>
Sat, 27 Mar 2021 18:45:12 +0000 (14:45 -0400)
Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>
Open-ILS/src/eg2/src/app/staff/admin/acq/currency/currencies.component.html
Open-ILS/src/eg2/src/app/staff/admin/acq/currency/currencies.component.ts
Open-ILS/src/eg2/src/app/staff/admin/acq/currency/currencies.module.ts
Open-ILS/src/eg2/src/app/staff/admin/acq/currency/exchange-rates-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/acq/currency/exchange-rates-dialog.component.ts [new file with mode: 0644]

index f585cc8..efef63c 100644 (file)
   </eg-grid-toolbar-button>
   <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 label="Delete Selected" i18n-label (onClick)="deleteIfPossible($event)">
   </eg-grid-toolbar-action>
 
   <eg-grid-column path="code"></eg-grid-column>
   <eg-grid-column path="label"></eg-grid-column>
-
+  <ng-template #exchangeRatesTmpl let-currency="row">
+    <button class="btn btn-outline-secondary" (click)="openExchangeRatesDialog(currency.code())" i18n>Manage Exchange Rates</button>
+  </ng-template>
+  <eg-grid-column i18n-label label="Exchange Rates" name="exchange_rates" [cellTemplate]="exchangeRatesTmpl"></eg-grid-column>
 
 </eg-grid>
 
     [preloadLinkedValues]="true"
     [readonlyFields]="readonlyFields">
 </eg-fm-record-editor>
+
+<eg-exchange-rates-dialog #exchangeRatesDialog></eg-exchange-rates-dialog>
+
+<eg-confirm-dialog #confirmDel
+  dialogTitle="Delete?" i18n-dialogTitle
+  dialogBody="Delete currency?" i18n-dialogBody>
+</eg-confirm-dialog>
+<eg-alert-dialog #alertDialog
+ i18n-dialogBody
+  dialogBody="Currency cannot be deleted: it is in use by funds, providers, funding sources, debits, and/or exchange rates">
+</eg-alert-dialog>
index 1b1c056..72d3e4e 100644 (file)
@@ -14,6 +14,10 @@ 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 {ExchangeRatesDialogComponent} from './exchange-rates-dialog.component';
+import {Observable, forkJoin, of} from 'rxjs';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
 
 @Component({
     templateUrl: './currencies.component.html'
@@ -24,6 +28,9 @@ export class CurrenciesComponent extends AdminPageComponent implements OnInit {
     classLabel: string;
 
     @ViewChild('grid', { static: true }) grid: GridComponent;
+    @ViewChild('exchangeRatesDialog', { static: false }) exchangeRatesDialog: ExchangeRatesDialogComponent;
+    @ViewChild('alertDialog', {static: false}) private alertDialog: AlertDialogComponent;
+    @ViewChild('confirmDel', { static: true }) confirmDel: ConfirmDialogComponent;
 
     cellTextGenerator: GridCellTextGenerator;
 
@@ -45,7 +52,7 @@ export class CurrenciesComponent extends AdminPageComponent implements OnInit {
 
     ngOnInit() {
         this.cellTextGenerator = {
-            name: row => row.name()
+            exchange_rates: row => ''
         };
         this.fieldOrder = 'code,name';
         this.defaultNewRecord = this.idl.create('acqct');
@@ -100,4 +107,43 @@ export class CurrenciesComponent extends AdminPageComponent implements OnInit {
         this.includeOrgDescendants = true;
     }
 
+    openExchangeRatesDialog(code: string) {
+        this.exchangeRatesDialog.currencyCode = code;
+        this.exchangeRatesDialog.open({size: 'lg'});
+    }
+
+    deleteIfPossible(rows: IdlObject[]) {
+        if (rows.length > 0) {
+            const code = rows[0].code();
+            let can: boolean = true;
+            forkJoin([
+                this.pcrud.search('acqexr',  { from_currency: code }, { limit: 1 }, { atomic: true }),
+                this.pcrud.search('acqexr',  { to_currency: code },   { limit: 1 }, { atomic: true }),
+                this.pcrud.search('acqf',    { currency_type: code }, { limit: 1 }, { atomic: true }),
+                this.pcrud.search('acqpro',  { currency_type: code }, { limit: 1 }, { atomic: true }),
+                this.pcrud.search('acqfdeb', { origin_currency_type: code }, { limit: 1 }, { atomic: true }),
+                this.pcrud.search('acqfs',   { currency_type: code }, { 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();
+                    }
+                }
+            );
+        }
+    }
+
 }
index 54cd1ce..9b4f258 100644 (file)
@@ -3,10 +3,12 @@ import {StaffCommonModule} from '@eg/staff/common.module';
 import {AdminCommonModule} from '@eg/staff/admin/common.module';
 import {CurrenciesRoutingModule} from './routing.module';
 import {CurrenciesComponent} from './currencies.component';
+import {ExchangeRatesDialogComponent} from './exchange-rates-dialog.component';
 
 @NgModule({
   declarations: [
-    CurrenciesComponent
+    CurrenciesComponent,
+    ExchangeRatesDialogComponent
   ],
   imports: [
     StaffCommonModule,
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/currency/exchange-rates-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/admin/acq/currency/exchange-rates-dialog.component.html
new file mode 100644 (file)
index 0000000..1459072
--- /dev/null
@@ -0,0 +1,49 @@
+<eg-string #successString i18n-text text="Exchange Rates Update Succeeded"></eg-string>
+<eg-string #updateFailedString i18n-text text="Exchange Rates Update Failed"></eg-string>
+
+<ng-template #dialogContent>
+  <div class="modal-header bg-info" *ngIf="doneLoading">
+    <h3 *ngIf="canUpdate" class="modal-title" i18n>Manage Exchange Rates for {{currency.code()}}: {{currency.label()}}</h3>
+    <h3 *ngIf="!canUpdate" class="modal-title" i18n>View Exchange Rates for {{currency.code()}}: {{currency.label()}}</h3>
+    <button type="button" class="close"
+      i18n-aria-label aria-label="Close" (click)="close()">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body" [hidden]="!doneLoading">
+    <h4 i18n>Exchange rates to other currencies: 1 {{currency?.label()}} is equal to:</h4>
+    <div *ngIf="otherCurrencies?.length < 1" class="alert alert-warning" i18n>
+      No other currencies are currently defined, so cannot set any exchange rates.
+    </div>
+    <form #exrForm="ngForm" role="form" [hidden]="otherCurrencies?.length < 1" class="form-validated">
+      <div class="form-group row mt-2" *ngFor="let ratio of ratios">
+        <label for="exr-{{ratio.to_currency().code()}}" class="col-sm-4 col-form-label">
+          {{ratio.to_currency().code()}} ({{ratio.to_currency().label()}})
+        </label>
+        <div class="col-sm-3">
+          <input *ngIf="canUpdate" class="form-control" type="number" id="exr-{{ratio.to_currency().code()}}"
+            [disabled]="ratio.id() === -1"
+            [ngModel]="ratio.ratio()" name="ratio_{{ratio.to_currency().code()}}"
+            (ngModelChange)="ratio.ratio($event)">
+          <span class="form-control-plaintext" *ngIf="!canUpdate">
+            <ng-container *ngIf="ratio.ratio() !== undefined">{{ratio.ratio()}}</ng-container>
+            <ng-container *ngIf="ratio.ratio() === undefined" i18n>not set</ng-container>
+          </span>
+        </div>
+        <div class="col-sm-3">
+          <span *ngIf="ratio.id() === -1" class="alert-warning" i18n>(inverse; go to other currency to change)</span>
+        </div>
+      </div>
+    </form>
+  </div>
+  <div class="modal-footer">
+    <ng-container *ngIf="canUpdate">
+      <button type="button" class="btn btn-info" [disabled]="!(exrForm?.dirty)"
+        (click)="save()" i18n>Save</button>
+      </ng-container>
+    <button type="button" class="btn btn-warning" *ngIf="canUpdate"
+      (click)="close()" i18n>Cancel</button>
+    <button type="button" class="btn btn-warning" *ngIf="!canUpdate"
+      (click)="close()" i18n>Close</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/currency/exchange-rates-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/acq/currency/exchange-rates-dialog.component.ts
new file mode 100644 (file)
index 0000000..2b5c77c
--- /dev/null
@@ -0,0 +1,138 @@
+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';
+
+@Component({
+  selector: 'eg-exchange-rates-dialog',
+  templateUrl: './exchange-rates-dialog.component.html'
+})
+
+export class ExchangeRatesDialogComponent
+  extends DialogComponent implements OnInit {
+
+    @Input() currencyCode: string;
+    currency: IdlObject;
+    otherCurrencies: IdlObject[];
+    existingRatios: {[toCurrency: string]: IdlObject} = {}
+    existingInverseRatios: {[fromCurrency: string]: IdlObject} = {}
+    ratios: IdlObject[];
+    idlDef: any;
+    fieldOrder: any;
+    canUpdate: boolean = false;
+    doneLoading: boolean = false;
+
+    @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 perm: PermService,
+        private toast: ToastService,
+        private modal: NgbModal
+    ) {
+        super(modal);
+    }
+    
+    ngOnInit() {
+        this.currency = null;
+        this.onOpen$.subscribe(() => this._initRecord());
+        this.idlDef = this.idl.classes['acqct'];
+        this.perm.hasWorkPermAt(['ADMIN_CURRENCY_TYPE'], true).then((perm) => {
+            if (perm['ADMIN_CURRENCY_TYPE'].length > 0) {
+                this.canUpdate = true;
+            }
+        });
+    }
+
+    private _initRecord() {
+        this.doneLoading = false;
+        this.ratios = [];
+        this.otherCurrencies = [];
+        this.existingRatios = {};
+        this.existingInverseRatios = {};
+        this.pcrud.retrieve('acqct', this.currencyCode, {}
+        ).subscribe(res => this.currency = res);
+        this.pcrud.search('acqexr', { from_currency: this.currencyCode }, {
+            flesh: 1,
+            flesh_fields: {'acqexr': ['to_currency']},
+        }, {}).subscribe(
+            exr => this.existingRatios[exr.to_currency().code()] = exr,
+            err => {},
+            () => this.pcrud.search('acqexr', { to_currency: this.currencyCode }, {
+                                     flesh: 1,
+                                     flesh_fields: {'acqexr': ['from_currency']},
+                                   }, {}).subscribe(
+                    exr => this.existingInverseRatios[exr.from_currency().code()] = exr,
+                    err => {},
+                    () =>  this.pcrud.search('acqct', { code: { '!=': this.currencyCode } },
+                                            { order_by: 'code ASC' }, { atomic: true })
+                                           .subscribe(
+                                             currs => this.otherCurrencies = currs,
+                                             err => {},
+                                           () => { this._mergeCurrenciesAndRates(); this.doneLoading = true; }
+                                           )
+                    )
+        );
+    }
+
+    private _mergeCurrenciesAndRates() {
+        this.ratios = [];
+        this.otherCurrencies.forEach(curr => {
+            if (curr.code() in this.existingRatios) {
+                this.ratios.push(this.existingRatios[curr.code()]);
+            } else if (curr.code() in this.existingInverseRatios) {
+                const ratio = this.idl.clone(this.existingInverseRatios[curr.code()]);
+                // mark it as an inverse ratio that should not be directly edited
+                ratio.id(-1);
+                const toCur = ratio.to_currency();
+                ratio.to_currency(ratio.from_currency());
+                ratio.from_currency(toCur);
+                ratio.ratio(1.0/ratio.ratio());
+                this.ratios.push(ratio);
+            } else {
+                const ratio = this.idl.create('acqexr');
+                ratio.from_currency(this.currencyCode);
+                ratio.to_currency(curr);
+                this.ratios.push(ratio);
+            }
+        });
+    }
+
+    save() {
+        const updateBatch: IdlObject[] = [];
+        this.ratios.forEach(ratio => {
+            if (ratio.id() === -1) {
+                // ignore inverse entries
+            } else if (ratio.id() === undefined && ratio.ratio() !== undefined && ratio.ratio() !== null) {
+                // completely new entry
+                ratio.isnew(true);
+                updateBatch.push(ratio);
+            } else if (ratio.id() !== undefined && ratio.ratio() !== undefined && ratio.ratio() !== null) {
+                // entry that might have been updated
+                ratio.ischanged(true);
+                updateBatch.push(ratio);
+            } else if (ratio.id() !== undefined && (ratio.ratio() === undefined || ratio.ratio() === null)) {
+                // existing entry to delete
+                ratio.isdeleted(true);
+                updateBatch.push(ratio);
+            }
+        });
+        this.pcrud.autoApply(updateBatch).toPromise().then(res => this.close(res));
+    }
+
+}