LP1904036 add billings and more
authorBill Erickson <berickxx@gmail.com>
Mon, 1 Mar 2021 22:15:07 +0000 (17:15 -0500)
committerGalen Charlton <gmc@equinoxOLI.org>
Fri, 28 Oct 2022 00:13:25 +0000 (20:13 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Jane Sandberg <js7389@princeton.edu>
Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
Open-ILS/src/eg2/src/app/staff/circ/patron/items.component.html
Open-ILS/src/eg2/src/app/staff/circ/patron/items.component.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/patron.service.ts
Open-ILS/src/eg2/src/app/staff/share/circ/billing-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/circ/billing-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/circ/circ.module.ts
Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts
Open-ILS/src/eg2/src/app/staff/share/circ/grid.component.html
Open-ILS/src/eg2/src/app/staff/share/circ/grid.component.ts

index b890ff4..04c388a 100644 (file)
@@ -15,7 +15,7 @@
         <ng-container *ngIf="loading">
           <ng-container *ngTemplateOutlet="progress"></ng-container>
         </ng-container>
-        <eg-circ-grid #checkoutsGrid (reloadRequested)="load()">
+        <eg-circ-grid #checkoutsGrid [persistKey]="persistKey" (reloadRequested)="load()">
         </eg-circ-grid>
       </ng-template>
     </li>
@@ -27,7 +27,7 @@
             <ng-container *ngIf="loading">
               <ng-container *ngTemplateOutlet="progress"></ng-container>
             </ng-container>
-            <eg-circ-grid #otherGrid (reloadRequested)="load()">
+            <eg-circ-grid #otherGrid [persistKey]="persistKey" (reloadRequested)="load()">
             </eg-circ-grid>
           </ng-container>
         </ng-template>
@@ -45,7 +45,7 @@
             <ng-container *ngIf="loading">
               <ng-container *ngTemplateOutlet="progress"></ng-container>
             </ng-container>
-            <eg-circ-grid #nonCatGrid (reloadRequested)="load()">
+            <eg-circ-grid #nonCatGrid [persistKey]="persistKey" (reloadRequested)="load()">
             </eg-circ-grid>
           </ng-container>
         </ng-container>
index 40ee8fd..d645033 100644 (file)
@@ -42,6 +42,7 @@ export class ItemsComponent implements OnInit, AfterViewInit {
     displayClaimsReturned: number = null;
     fetchCheckedIn = true;
     displayAltList = true;
+    persistKey: string;
 
     @ViewChild('checkoutsGrid') private checkoutsGrid: CircGridComponent;
     @ViewChild('otherGrid') private otherGrid: CircGridComponent;
@@ -61,23 +62,29 @@ export class ItemsComponent implements OnInit, AfterViewInit {
     ) {}
 
     ngOnInit() {
-        this.load();
+        this.load(true);
     }
 
     ngAfterViewInit() {
     }
 
-    load(): Promise<any> {
+    load(firstLoad?: boolean): Promise<any> {
         this.loading = true;
-        return this.applyDisplaySettings()
-        .then(_ => this.loadTab(this.itemsTab));
+
+        if (firstLoad) {
+            return this.applyDisplaySettings()
+            .then(_ => this.loadTab(this.itemsTab));
+        } else {
+            return this.loadTab(this.itemsTab)
+            .then(_ => this.context.refreshPatron());
+        }
     }
 
     tabChange(evt: NgbNavChangeEvent) {
         setTimeout(() => this.loadTab(evt.nextId));
     }
 
-    loadTab(name: string) {
+    loadTab(name: string): Promise<any> {
         this.loading = true;
         let promise;
         if (name === 'checkouts') {
@@ -88,7 +95,9 @@ export class ItemsComponent implements OnInit, AfterViewInit {
             promise = this.loadNonCatGrid();
         }
 
-        promise.then(_ => this.loading = false);
+        this.persistKey = `circ.patron.items.${name}`;
+
+        return promise.then(_ => this.loading = false);
     }
 
     applyDisplaySettings(): Promise<any> {
index 4e6fcb4..8eb24ab 100644 (file)
@@ -110,6 +110,13 @@ export class PatronContextService {
         this.loaded = false;
         this.patron = null;
         this.checkouts = [];
+        return this.refreshPatron(id).then(_ => this.loaded = true);
+    }
+
+    // Update the patron data without resetting all of the context data.
+    refreshPatron(id?: number): Promise<any> {
+        if (!id) { id = this.patron.id(); }
+
         this.alerts = new PatronAlerts();
 
         return this.net.request(
@@ -118,8 +125,7 @@ export class PatronContextService {
             this.auth.token(), id, PATRON_FLESH_FIELDS).toPromise()
         .then(p => this.patron = p)
         .then(_ => this.getPatronStats(id))
-        .then(_ => this.compileAlerts())
-        .then(_ => this.loaded = true);
+        .then(_ => this.compileAlerts());
     }
 
     getPatronStats(id: number): Promise<any> {
diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/billing-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/circ/billing-dialog.component.html
new file mode 100644 (file)
index 0000000..c957ed6
--- /dev/null
@@ -0,0 +1,80 @@
+<eg-string #successMsg text="Successfully Added Billing" i18n-text></eg-string>
+<eg-string #errorMsg text="Failed To Add Billing" i18n-text></eg-string>
+
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>
+      Bill Patron: 
+        {{xact.usr().family_name()}}, 
+        {{xact.usr().first_given_name()}} : 
+        {{xact.usr().card().barcode()}}
+    </h4>
+    <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">
+
+    <div class="row">
+      <div class="col-lg-2" i18n>Bill #</div>
+      <div class="col-lg-4">{{xact.id()}}</div>
+      <div class="col-lg-4" i18n>Total Billed</div>
+      <div class="col-lg-2">{{xact.summary().total_owed() | currency}}</div>
+    </div>
+    <div class="row">
+      <div class="col-lg-2" i18n>Type</div>
+      <div class="col-lg-4">{{xact.summary().xact_type()}}</div>
+      <div class="col-lg-4" i18n>Total Paid</div>
+      <div class="col-lg-2">{{xact.summary().total_paid() | currency}}</div>
+    </div>
+    <div class="row">
+      <div class="col-lg-2" i18n>Start</div>
+      <div class="col-lg-4">{{xact.xact_start() | date:'short'}}</div>
+      <div class="col-lg-4" i18n>Balance Owed</div>
+      <div class="col-lg-2">{{xact.summary().balance_owed() | currency}}</div>
+    </div>
+    <div class="row">
+      <div class="col-lg-2" i18n>Finish</div>
+      <div class="col-lg-4">{{xact.xact_finish() | date:'short'}}</div>
+      <div class="col-lg-4" i18n>Renewal?</div>
+      <div class="col-lg-2"><eg-bool [value]="isRenewal()"></eg-bool></div>
+    </div>
+
+    <hr/>
+
+    <div class="form-validated">
+      <div class="row mt-2">
+        <div class="col-lg-4" i18n>Location</div>
+        <div class="col-lg-8" i18n>{{hereOrg}}</div>
+      </div>
+      <div class="row mt-2">
+        <div class="col-lg-4" i18n>Billing Type</div>
+        <div class="col-lg-8">
+          <eg-combobox #bTypeCbox [entries]="billingTypes" 
+            [required]="true" (onChange)="btChanged($event)"></eg-combobox>
+        </div>
+      </div>
+      <div class="row mt-2">
+        <div class="col-lg-4" i18n>Amount</div>
+        <div class="col-lg-8" i18n>
+          <input type="number" class="form-control" 
+            required [(ngModel)]="amount" [min]="0"/>
+        </div>
+      </div>
+      <div class="row mt-2">
+        <div class="col-lg-4" i18n>Note</div>
+        <div class="col-lg-8" i18n>
+          <textarea class="form-control" [rows]="3" [(ngModel)]="note"></textarea>
+        </div>
+      </div>
+    </div>
+
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-success" [disabled]="!saveable()"
+      (click)="submit()" i18n>Submit Bill</button>
+    <button type="button" class="btn btn-warning"
+      (click)="close()" i18n>Cancel</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/billing-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/circ/billing-dialog.component.ts
new file mode 100644 (file)
index 0000000..6b20e19
--- /dev/null
@@ -0,0 +1,108 @@
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs';
+import {switchMap} from 'rxjs/operators';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {AuthService} from '@eg/core/auth.service';
+import {OrgService} from '@eg/core/org.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {StringComponent} from '@eg/share/string/string.component';
+import {ComboboxEntry, ComboboxComponent} from '@eg/share/combobox/combobox.component';
+import {CircService} from './circ.service';
+
+/* Add a billing to a transaction */
+
+@Component({
+  selector: 'eg-add-billing-dialog',
+  templateUrl: 'billing-dialog.component.html'
+})
+
+export class AddBillingDialogComponent
+    extends DialogComponent implements OnInit {
+
+    @Input() xactId: number;
+
+    xact: IdlObject;
+    billingType: ComboboxEntry;
+    billingTypes: ComboboxEntry[] = [];
+    hereOrg: string;
+    amount: number;
+    note: string;
+
+    @ViewChild('successMsg') private successMsg: StringComponent;
+    @ViewChild('errorMsg') private errorMsg: StringComponent;
+    @ViewChild('bTypeCbox') private bTypeCbox: ComboboxComponent;
+
+    constructor(
+        private modal: NgbModal, // required for passing to parent
+        private toast: ToastService,
+        private net: NetService,
+        private evt: EventService,
+        private pcrud: PcrudService,
+        private circ: CircService,
+        private org: OrgService,
+        private auth: AuthService) {
+        super(modal);
+    }
+
+    ngOnInit() {
+        this.circ.getBillingTypes().then(types => {
+            this.billingTypes = types.map(bt => {
+                return {id: bt.id(), label: bt.name(), fm: bt};
+            });
+        });
+
+        this.hereOrg = this.org.get(this.auth.user().ws_ou()).shortname();
+
+        this.onOpen$.subscribe(_ => {
+            this.amount = null;
+            this.note = '';
+            //this.bTypeCbox.selectedId = 101; // Stock "Misc"
+            const node = document.getElementById('amount-input');
+            if (node) { node.focus(); }
+        });
+    }
+
+    open(options: NgbModalOptions = {}): Observable<any> {
+
+        // Fetch the xact data before opening the dialog.
+        return this.pcrud.retrieve('mbt', this.xactId, {
+            flesh: 2,
+            flesh_fields: {
+                mbt: ['usr', 'summary', 'circulation'],
+                au: ['card']
+            }
+        }).pipe(switchMap(xact => {
+            this.xact = xact;
+            return super.open(options);
+        }));
+    }
+
+    isRenewal(): boolean {
+        return (
+            this.xact &&
+            this.xact.circulation() &&
+            this.xact.circulation().parent_circ() !== null
+        );
+    }
+
+    btChanged(entry: ComboboxEntry) {
+        this.billingType = entry;
+        if (entry && entry.fm.default_price()) {
+            this.amount = entry.fm.default_price();
+        }
+    }
+
+    saveable(): boolean {
+        return this.billingType && this.amount > 0;
+    }
+
+    submit() {
+        this.close();
+    }
+}
+
index a067588..3cf3790 100644 (file)
@@ -8,6 +8,7 @@ import {PrecatCheckoutDialogComponent} from './precat-dialog.component';
 import {ClaimsReturnedDialogComponent} from './claims-returned-dialog.component';
 import {CircComponentsComponent} from './components.component';
 import {CircEventsComponent} from './events-dialog.component';
+import {AddBillingDialogComponent} from './billing-dialog.component';
 
 @NgModule({
     declarations: [
@@ -16,7 +17,8 @@ import {CircEventsComponent} from './events-dialog.component';
         DueDateDialogComponent,
         PrecatCheckoutDialogComponent,
         ClaimsReturnedDialogComponent,
-        CircEventsComponent
+        CircEventsComponent,
+        AddBillingDialogComponent
     ],
     imports: [
         StaffCommonModule,
@@ -24,7 +26,8 @@ import {CircEventsComponent} from './events-dialog.component';
     ],
     exports: [
         CircGridComponent,
-        CircComponentsComponent
+        CircComponentsComponent,
+        AddBillingDialogComponent
     ],
     providers: [
         CircService
index 3b84242..30f8699 100644 (file)
@@ -157,6 +157,7 @@ export class CircService {
 
     components: CircComponentsComponent;
     nonCatTypes: IdlObject[] = null;
+    billingTypes: IdlObject[] = null;
     autoOverrideCheckoutEvents: {[textcode: string]: boolean} = {};
     suppressCheckinPopups = false;
     ignoreCheckinPrecats = false;
@@ -184,6 +185,21 @@ export class CircService {
         ).toPromise().then(types => this.nonCatTypes = types);
     }
 
+    getBillingTypes(): Promise<IdlObject[]> {
+        if (this.billingTypes) {
+            return Promise.resolve(this.billingTypes);
+        }
+
+        return this.pcrud.search('cbt',
+            {
+                id: {'>': 100}, // first 100 are reserved
+                owner: this.org.fullPath(this.auth.user().ws_ou(), true)
+            },
+            {order_by: {cbt: 'name'}},
+            {atomic: true}
+        ).toPromise().then(types => this.billingTypes = types);
+    }
+
     // Remove internal tracking variables on Param objects so they are
     // not sent to the server, which can result in autoload errors.
     apiParams(
@@ -410,8 +426,7 @@ export class CircService {
             case 'SUCCESS':
             case 'NO_CHANGE':
                 this.audio.play('success.checkin');
-                // TODO do copy status stuff
-                break;
+                return this.handleCheckinSuccess(result);
 
             case 'ITEM_NOT_CATALOGED':
                 this.audio.play('error.checkout.no_cataloged');
@@ -421,13 +436,15 @@ export class CircService {
                     return this.components.routeToCatalogingDialog.open()
                     .toPromise().then(_ => result);
                 }
-
-                // alert, etc.
         }
 
         return Promise.resolve(result);
     }
 
+    handleCheckinSuccess(result: CheckinResult): Promise<CheckinResult> {
+        return Promise.resolve(result);
+    }
+
     handleOverridableCheckinEvents(
         result: CheckinResult, events: EgEvent[]): Promise<CheckinResult> {
         const params = result.params;
index b89311a..28353ae 100644 (file)
@@ -17,6 +17,7 @@
   dialogTitle="Claims Never Checked Out"
   dialogBody="Mark {{claimsNeverCount}} items as Never Checked Out?">
 </eg-confirm-dialog>
+<eg-add-billing-dialog #addBillingDialog></eg-add-billing-dialog>
 
 
 <ng-template #titleTemplate let-r="row">
@@ -34,9 +35,8 @@
 
 <eg-grid #circGrid [dataSource]="gridDataSource" [sortable]="true"
   [rowFlairIsEnabled]="true" [rowFlairCallback]="rowFlair"
-  [rowClassCallback]="rowClass"
-  [useLocalSort]="true" [cellTextGenerator]="cellTextGenerator"
-  [disablePaging]="true" [persistKey]="persistKey">
+  [rowClassCallback]="rowClass" [persistKey]="persistKey"
+  [useLocalSort]="true" [cellTextGenerator]="cellTextGenerator">
 
   <eg-grid-toolbar-action
     i18n-group group="View" i18n-label label="Print Item Receipt(s)"
   </eg-grid-toolbar-action>
 
   <eg-grid-toolbar-action
-    i18n-group group="Edit" i18n-label label="Edit Due Date"
+    i18n-group group="Add" i18n-label label="Add Billing"
+    (onClick)="openBillingDialog($event)">
+  </eg-grid-toolbar-action>
+
+  <eg-grid-toolbar-action
+    i18n-group group="Circulation" i18n-label label="Edit Due Date"
     (onClick)="editDueDate($event)">
   </eg-grid-toolbar-action>
 
index cd22019..59b57d9 100644 (file)
@@ -31,6 +31,7 @@ import {MarkMissingDialogComponent
     } from '@eg/staff/share/holdings/mark-missing-dialog.component';
 import {ClaimsReturnedDialogComponent} from './claims-returned-dialog.component';
 import {ToastService} from '@eg/share/toast/toast.service';
+import {AddBillingDialogComponent} from './billing-dialog.component';
 
 export interface CircGridEntry {
     index: string; // class + id -- row index
@@ -113,6 +114,8 @@ export class CircGridComponent implements OnInit {
         private progressDialog: ProgressDialogComponent;
     @ViewChild('claimsReturnedDialog')
         private claimsReturnedDialog: ClaimsReturnedDialogComponent;
+    @ViewChild('addBillingDialog')
+        private addBillingDialog: AddBillingDialogComponent;
 
     constructor(
         private org: OrgService,
@@ -132,7 +135,12 @@ export class CircGridComponent implements OnInit {
         // The grid never fetches data directly.
         // The caller is responsible initiating all data loads.
         this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
-            return this.entries ? from(this.entries) : empty();
+            if (!this.entries) { return empty(); }
+
+            const page = this.entries.slice(pager.offset, pager.offset + pager.limit)
+                .filter(entry => entry !== undefined);
+
+            return from(page);
         };
 
         this.cellTextGenerator = {
@@ -367,7 +375,14 @@ export class CircGridComponent implements OnInit {
         if (!circ) { return false; } // noncat
 
         if (row.overdue === undefined) {
-            row.overdue = (Date.parse(circ.due_date()) < this.nowDate);
+
+            if (circ.stop_fines() &&
+                // Items that aren't really checked out can't be overdue.
+                circ.stop_fines().match(/LOST|CLAIMSRETURNED|CLAIMSNEVERCHECKEDOUT/)) {
+                row.overdue = false;
+            } else {
+                row.overdue = (Date.parse(circ.due_date()) < this.nowDate);
+            }
         }
         return row.overdue;
     }
@@ -567,5 +582,27 @@ export class CircGridComponent implements OnInit {
             );
         });
     }
+
+    openBillingDialog(rows: CircGridEntry[]) {
+
+        let changesApplied = false;
+
+        from(this.getCircIds(rows))
+        .pipe(concatMap(id => {
+            this.addBillingDialog.xactId = id;
+            return this.addBillingDialog.open();
+        }))
+        .subscribe(
+            changes => {
+                if (changes) { changesApplied = true; }
+            },
+            err => this.reportError(err),
+            ()  => {
+                if (changesApplied) {
+                    this.emitReloadRequest();
+                }
+            }
+        );
+    }
 }