LP1904036 Edit due date; styling overdues
authorBill Erickson <berickxx@gmail.com>
Tue, 23 Feb 2021 16:54:28 +0000 (11:54 -0500)
committerGalen Charlton <gmc@equinoxOLI.org>
Fri, 28 Oct 2022 00:13:24 +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.ts
Open-ILS/src/eg2/src/app/staff/share/circ/circ.module.ts
Open-ILS/src/eg2/src/app/staff/share/circ/due-date-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/circ/due-date-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/circ/grid.component.html
Open-ILS/src/eg2/src/app/staff/share/circ/grid.component.ts
Open-ILS/src/eg2/src/styles.css

index bc275c7..e342b1a 100644 (file)
@@ -135,7 +135,7 @@ export class ItemsComponent implements OnInit, AfterViewInit {
                 this.altList = this.altList.concat(list);
             }
         } else {
-            if (4 & displayCode) return;  // bitflag 4 == hide on checkin
+            if (4 & displayCode) { return; }  // bitflag 4 == hide on checkin
             this.altList = this.altList.concat(list);
         }
     }
index 02d0b9a..af4904f 100644 (file)
@@ -3,10 +3,12 @@ import {StaffCommonModule} from '@eg/staff/common.module';
 import {HoldingsModule} from '@eg/staff/share/holdings/holdings.module';
 import {CircService} from './circ.service';
 import {CircGridComponent} from './grid.component';
+import {DueDateDialogComponent} from './due-date-dialog.component';
 
 @NgModule({
     declarations: [
-        CircGridComponent
+        CircGridComponent,
+        DueDateDialogComponent
     ],
     imports: [
         StaffCommonModule,
diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/due-date-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/circ/due-date-dialog.component.html
new file mode 100644 (file)
index 0000000..f47258f
--- /dev/null
@@ -0,0 +1,50 @@
+<eg-string #successMsg text="Successfully Modified Due Date" i18n-text></eg-string>
+<eg-string #errorMsg text="Failed To Modify Due Date" i18n-text></eg-string>
+
+<ng-template #dialogContent>
+    <div class="modal-header bg-info">
+      <h4 class="modal-title">
+        <span i18n>Modify Due Date</span>
+      </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">
+      <h5 i18n>Modifying Due Date For {{circs.length}} Circulation(s)</h5>
+
+      <div class="row mt-3">
+        <div class="col-lg-3" i18n>New Date</div>
+        <div class="col-lg-9">
+          <eg-datetime-select [required]="true" (onChangeAsIso)="dueDateChange($event)">
+          </eg-datetime-select>
+        </div>
+      </div>
+
+      <div class="row mt-3" *ngIf="!dueDateIso">
+        <div class="col-lg-12 alert-danger" i18n>
+          Selected due date is not valid.
+        </div>
+      </div>
+
+      <div class="row mt-3" *ngIf="numSucceeded > 0">
+        <div class="col-lg-12" i18n>
+          {{numSucceeded}} Due Date(s) Successfully Modified
+        </div>
+      </div>
+      <div class="row mt-3" *ngIf="numFailed > 0">
+        <div class="col-lg-12">
+          <div class="alert alert-warning">
+            {{numFailed}} Due Date(s) Failed to Modify
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <button type="button" class="btn btn-warning"
+        (click)="close()" i18n>Cancel</button>
+      <button type="button" class="btn btn-success" [disabled]="!dueDateIso"
+        (click)="modifyBatch()" i18n>Modify</button>
+    </div>
+  </ng-template>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/due-date-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/circ/due-date-dialog.component.ts
new file mode 100644 (file)
index 0000000..60ced72
--- /dev/null
@@ -0,0 +1,95 @@
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs';
+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 {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} from '@eg/share/combobox/combobox.component';
+
+/* Dialog for modifying circulation due dates. */
+
+@Component({
+  selector: 'eg-due-date-dialog',
+  templateUrl: 'due-date-dialog.component.html'
+})
+
+export class DueDateDialogComponent
+    extends DialogComponent implements OnInit {
+
+    @Input() circs: IdlObject[] = [];
+    @ViewChild('successMsg', { static: true }) private successMsg: StringComponent;
+    @ViewChild('errorMsg', { static: true }) private errorMsg: StringComponent;
+
+    dueDateIsValid = false;
+    dueDateIso: string;
+    numSucceeded: number;
+    numFailed: number;
+    nowTime: number;
+
+    constructor(
+        private modal: NgbModal, // required for passing to parent
+        private toast: ToastService,
+        private net: NetService,
+        private evt: EventService,
+        private pcrud: PcrudService,
+        private auth: AuthService) {
+        super(modal); // required for subclassing
+    }
+
+    ngOnInit() {
+        this.onOpen$.subscribe(_ => {
+            this.numSucceeded = 0;
+            this.numFailed = 0;
+            this.dueDateIso = new Date().toISOString();
+            this.nowTime = new Date().getTime();
+        });
+    }
+
+    dueDateChange(iso: string) {
+        if (iso && Date.parse(iso) > this.nowTime) {
+            this.dueDateIso = iso;
+        } else {
+            this.dueDateIso = null;
+        }
+    }
+
+    modifyBatch() {
+        if (!this.dueDateIso) { return; }
+
+        let promise = Promise.resolve();
+
+        this.circs.forEach(circ => {
+            promise = promise.then(_ => this.modifyOne(circ));
+        });
+
+        promise.then(_ => {
+            this.close();
+            this.circs = [];
+        });
+    }
+
+    modifyOne(circ: IdlObject): Promise<any> {
+        return this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.circulation.due_date.update',
+            this.auth.token(), circ.id(), this.dueDateIso
+
+        ).toPromise().then(modCirc => {
+
+            const evt = this.evt.parse(modCirc);
+
+            if (evt) {
+                this.numFailed++;
+                console.error(evt);
+            } else {
+                this.numSucceeded++;
+                this.respond(modCirc);
+            }
+        });
+    }
+}
index 410cbf5..1a7ab98 100644 (file)
@@ -1,6 +1,8 @@
 
 <eg-progress-dialog #progressDialog></eg-progress-dialog>
 <eg-copy-alerts-dialog #copyAlertsDialog></eg-copy-alerts-dialog>
+<eg-string #overdueString i18n-text text="Overdue"></eg-string>
+<eg-due-date-dialog #dueDateDialog></eg-due-date-dialog>
 
 <ng-template #titleTemplate let-r="row">
   <ng-container *ngIf="r.record">
@@ -8,8 +10,16 @@
   </ng-container>
   <ng-container *ngIf="!r.record">{{r.title}}</ng-container>
 </ng-template>
+<ng-template #barcodeTemplate let-r="row">
+  <ng-container *ngIf="r.copy">
+    <a href="/eg/staff/cat/item/{{r.copy.id()}}">{{r.copy.barcode()}}</a>
+  </ng-container>
+</ng-template>
+
 
 <eg-grid #circGrid [dataSource]="gridDataSource" [sortable]="true"
+  [rowFlairIsEnabled]="true" [rowFlairCallback]="rowFlair"
+  [rowClassCallback]="rowClass"
   [useLocalSort]="true" [cellTextGenerator]="cellTextGenerator"
   [disablePaging]="true" [persistKey]="persistKey">
 
     (onClick)="openItemAlerts($event, 'create')">
   </eg-grid-toolbar-action>
 
+  <eg-grid-toolbar-action
+    i18n-group group="Edit" i18n-label label="Edit Due Date"
+    (onClick)="editDueDate($event)">
+  </eg-grid-toolbar-action>
+
   <eg-grid-column [index]="true" path="index" [hidden]="true"
     label="Row Index" i18n-label></eg-grid-column>
 
@@ -35,7 +50,8 @@
   <eg-grid-column path="dueDate" label="Due Date" i18n-label
     datatype="timestamp"></eg-grid-column>
 
-  <eg-grid-column path="copy.barcode" label="Barcode" i18n-label></eg-grid-column>
+  <eg-grid-column path="copy.barcode" label="Barcode" i18n-label
+    [cellTemplate]="barcodeTemplate"></eg-grid-column>
 
   <eg-grid-column path="title" label="Title" i18n-label 
     [cellTemplate]="titleTemplate"></eg-grid-column>
index fd489f3..da0f97b 100644 (file)
@@ -8,7 +8,8 @@ import {NetService} from '@eg/core/net.service';
 import {PcrudService} from '@eg/core/pcrud.service';
 import {CheckoutParams, CheckoutResult, CircService} from './circ.service';
 import {PromptDialogComponent} from '@eg/share/dialog/prompt.component';
-import {GridDataSource, GridColumn, GridCellTextGenerator} from '@eg/share/grid/grid';
+import {GridDataSource, GridColumn, GridCellTextGenerator,
+    GridRowFlairEntry} from '@eg/share/grid/grid';
 import {GridComponent} from '@eg/share/grid/grid.component';
 import {Pager} from '@eg/share/util/pager';
 import {StoreService} from '@eg/core/store.service';
@@ -18,6 +19,8 @@ import {CopyAlertsDialogComponent
     } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
 import {ArrayUtil} from '@eg/share/util/array';
 import {PrintService} from '@eg/share/print/print.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {DueDateDialogComponent} from './due-date-dialog.component';
 
 export interface CircGridEntry {
     index: string; // class + id -- row index
@@ -31,6 +34,10 @@ export interface CircGridEntry {
     dueDate?: string;
     copyAlertCount?: number;
     nonCatCount?: number;
+
+    // useful for reporting precaculated values and avoiding
+    // repetitive date creation on grid render.
+    overdue?: boolean;
 }
 
 const CIRC_FLESH_DEPTH = 4;
@@ -63,10 +70,16 @@ export class CircGridComponent implements OnInit {
     entries: CircGridEntry[] = null;
     gridDataSource: GridDataSource = new GridDataSource();
     cellTextGenerator: GridCellTextGenerator;
+    rowFlair: (row: CircGridEntry) => GridRowFlairEntry;
+    rowClass: (row: CircGridEntry) => string;
+
+    nowDate: number = new Date().getTime();
 
+    @ViewChild('overdueString') private overdueString: StringComponent;
     @ViewChild('circGrid') private circGrid: GridComponent;
     @ViewChild('copyAlertsDialog')
         private copyAlertsDialog: CopyAlertsDialogComponent;
+    @ViewChild('dueDateDialog') private dueDateDialog: DueDateDialogComponent;
 
     constructor(
         private org: OrgService,
@@ -84,15 +97,24 @@ 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[]) => {
-            if (this.entries) {
-                return from(this.entries);
-            } else {
-                return empty();
-            }
+            return this.entries ? from(this.entries) : empty();
         };
 
         this.cellTextGenerator = {
-            title: row => row.title
+            title: row => row.title,
+            barcode: row => row.copy ? row.copy.barcode() : ''
+        };
+
+        this.rowFlair = (row: CircGridEntry) => {
+            if (this.circIsOverdue(row)) {
+                return {icon: 'error_outline', title: this.overdueString.text};
+            }
+        };
+
+        this.rowClass = (row: CircGridEntry) => {
+            if (this.circIsOverdue(row)) {
+                return 'less-intense-alert';
+            }
         };
     }
 
@@ -200,6 +222,14 @@ export class CircGridComponent implements OnInit {
         );
     }
 
+    getCopies(rows: any): IdlObject[] {
+        return rows.filter(r => r.copy).map(r => r.copy);
+    }
+
+    getCircs(rows: any): IdlObject[] {
+        return rows.filter(r => r.circ).map(r => r.circ);
+    }
+
     printReceipts(rows: any) {
         if (rows.length > 0) {
             this.printer.print({
@@ -209,5 +239,39 @@ export class CircGridComponent implements OnInit {
             });
         }
     }
+
+    editDueDate(rows: any) {
+        const circs = this.getCircs(rows);
+        if (circs.length === 0) { return; }
+
+        let refreshNeeded = false;
+        this.dueDateDialog.circs = circs;
+        this.dueDateDialog.open().subscribe(
+            circ => {
+                refreshNeeded = true;
+                const row = rows.filter(r => r.circ.id() === circ.id())[0];
+                row.circ.due_date(circ.due_date());
+                row.dueDate = circ.due_date();
+                delete row.overdue; // it will recalculate
+            },
+            err => console.error(err),
+            () => {
+                if (refreshNeeded) {
+                    this.reloadGrid();
+                }
+            }
+        );
+    }
+
+    circIsOverdue(row: CircGridEntry): boolean {
+        const circ = row.circ;
+
+        if (!circ) { return false; } // noncat
+
+        if (row.overdue === undefined) {
+            row.overdue = (Date.parse(circ.due_date()) < this.nowDate);
+        }
+        return row.overdue;
+    }
 }
 
index 08f6959..bccc56b 100644 (file)
@@ -323,3 +323,12 @@ input.small {
   width: 4em;
 }
 
+/* 
+ * Created initially for styled grid rows where full 'bg-danger' CSS is
+ * intense and not especially readable, more so when rows are stacked.
+ * http://web-accessibility.carnegiemuseums.org/design/color/
+ */
+.less-intense-alert {
+  background-color: #f9dede;
+  color: black;
+}