LP1934164 egDueDate and egOrgDateInContext Angular pipes
authorBill Erickson <berickxx@gmail.com>
Tue, 29 Jun 2021 17:50:56 +0000 (13:50 -0400)
committerJane Sandberg <js7389@princeton.edu>
Wed, 5 Oct 2022 13:10:49 +0000 (06:10 -0700)
These support displaying dates in the timezone of a specified org unit.

Example:

{{circ.xact_start() | egOrgDateInContext:circ.circ_lib():circ.duration()}}

The format service also gets a dateOnlyIntervalField parameter to
display dates as dates or dates + time depending on whether the provided
duration is day-granular.

Also adds a handy pipe (egDueDate) which takes a circulation as its
value and collects the correct parameters to display the due date in the
correct time zone and with the correct dateOnlyIntervalField value.

Example:

{{circ | egDueDate}}

Includes Sandbox examples.

Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Jane Sandberg <js7389@princeton.edu>
Open-ILS/src/eg2/src/app/core/core.module.ts
Open-ILS/src/eg2/src/app/core/format.service.ts
Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts
Open-ILS/src/eg2/src/app/share/grid/grid.ts
Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts

index 40cfe29..431ca14 100644 (file)
@@ -6,18 +6,22 @@
  */
 import {NgModule} from '@angular/core';
 import {CommonModule, DatePipe, DecimalPipe} from '@angular/common';
-import {FormatService, FormatValuePipe} from './format.service';
+import {FormatValuePipe, OrgDateInContextPipe, DueDatePipe} from './format.service';
 
 @NgModule({
   declarations: [
-    FormatValuePipe
+    FormatValuePipe,
+    OrgDateInContextPipe,
+    DueDatePipe
   ],
   imports: [
     CommonModule
   ],
   exports: [
     CommonModule,
-    FormatValuePipe
+    FormatValuePipe,
+    OrgDateInContextPipe,
+    DueDatePipe
   ],
   providers: [
     DatePipe,
index 9622bd0..b73209b 100644 (file)
@@ -2,8 +2,10 @@ import {Injectable, Pipe, PipeTransform} from '@angular/core';
 import {DatePipe, DecimalPipe, getLocaleDateFormat, getLocaleTimeFormat, getLocaleDateTimeFormat, FormatWidth} from '@angular/common';
 import {IdlService, IdlObject} from '@eg/core/idl.service';
 import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
 import {LocaleService} from '@eg/core/locale.service';
 import * as moment from 'moment-timezone';
+import {DateUtil} from '@eg/share/util/date';
 
 /**
  * Format IDL vield values for display.
@@ -19,6 +21,7 @@ export interface FormatParams {
     orgField?: string; // 'shortname' || 'name'
     datePlusTime?: boolean;
     timezoneContextOrg?: number;
+    dateOnlyInterval?: string;
 }
 
 @Injectable({providedIn: 'root'})
@@ -27,12 +30,14 @@ export class FormatService {
     dateFormat = 'shortDate';
     dateTimeFormat = 'short';
     wsOrgTimezone: string = OpenSRF.tz;
+    tzCache: {[orgId: number]: string} = {};
 
     constructor(
         private datePipe: DatePipe,
         private decimalPipe: DecimalPipe,
         private idl: IdlService,
         private org: OrgService,
+        private auth: AuthService,
         private locale: LocaleService
     ) {
 
@@ -119,17 +124,35 @@ export class FormatService {
                     // local one
                     tz = 'UTC';
                 } else {
-                    tz = this.wsOrgTimezone;
+                    if (params.timezoneContextOrg) {
+                        tz = this.getOrgTz( // support ID or object
+                            this.org.get(params.timezoneContextOrg).id());
+                    } else {
+                        tz = this.wsOrgTimezone;
+                    }
                 }
+
                 const date = moment(value).tz(tz);
-                if (!date.isValid()) {
-                    console.error('Invalid date in format service', value);
+                if (!date || !date.isValid()) {
+                    console.error(
+                        'Invalid date in format service; date=', value, 'tz=', tz);
                     return '';
                 }
+
                 let fmt = this.dateFormat || 'shortDate';
+
                 if (params.datePlusTime) {
+                    // Time component directly requested
                     fmt = this.dateTimeFormat || 'short';
+
+                } else if (params.dateOnlyInterval) {
+                    // Time component displays for non-day-granular intervals.
+                    const secs = DateUtil.intervalToSeconds(params.dateOnlyInterval);
+                    if (secs !== null && secs % 86400 !== 0) {
+                        fmt = this.dateTimeFormat || 'short';
+                    }
                 }
+
                 return this.datePipe.transform(date.toISOString(true), fmt, date.format('ZZ'));
 
             case 'money':
@@ -153,6 +176,42 @@ export class FormatService {
                 return value + '';
         }
     }
+
+    /**
+    Fetch the org timezone from cache when available.  Otherwise,
+    get the timezone from the org unit setting.  The first time
+    this call is made, it may return the incorrect value since
+    it's not a promise-returning method (because format() is not a
+    promise-returning method).  Future calls will return the correct
+    value since it's locally cached.  Since most format() calls are
+    repeated many times for Angular digestion, the end result is that
+    the correct value will be used in the end.
+    */
+    getOrgTz(orgId: number): string {
+
+        if (this.tzCache[orgId] === null) {
+            // We are still waiting for the value to be returned
+            // from the server.
+            return this.wsOrgTimezone;
+        }
+
+        if (this.tzCache[orgId] !== undefined) {
+            // We have a cached value.
+            return this.tzCache[orgId];
+        }
+
+        // Avoid duplicate parallel lookups by indicating we
+        // are loading the value from the server.
+        this.tzCache[orgId] = null;
+
+        this.org.settings(['lib.timezone'], orgId)
+        .then(sets => this.tzCache[orgId] = sets['lib.timezone']);
+
+        // Use the local timezone while we wait for the real value
+        // to load from the server.
+        return this.wsOrgTimezone;
+    }
+
     /**
      * Create an IDL-friendly display version of a human-readable date
      */
@@ -310,3 +369,31 @@ export class FormatValuePipe implements PipeTransform {
     }
 }
 
+@Pipe({name: 'egOrgDateInContext'})
+export class OrgDateInContextPipe implements PipeTransform {
+    constructor(private formatter: FormatService) {}
+
+    transform(value: string, orgId?: number, interval?: string ): string {
+        return this.formatter.transform({
+            value: value,
+            datatype: 'timestamp',
+            timezoneContextOrg: orgId,
+            dateOnlyInterval: interval
+        });
+    }
+}
+
+@Pipe({name: 'egDueDate'})
+export class DueDatePipe implements PipeTransform {
+    constructor(private formatter: FormatService) {}
+
+    transform(circ: IdlObject): string {
+        return this.formatter.transform({
+            value: circ.due_date(),
+            datatype: 'timestamp',
+            timezoneContextOrg: circ.circ_lib(),
+            dateOnlyInterval: circ.duration()
+        });
+    }
+}
+
index 3822060..f3651f3 100644 (file)
@@ -40,6 +40,8 @@ export class GridColumnComponent implements OnInit {
     // Display using a specific OU's timestamp when datatype = timestamp
     @Input() timezoneContextOrg: number;
 
+    @Input() dateOnlyIntervalField: string;
+
     // Used in conjunction with cellTemplate
     @Input() cellContext: any;
     @Input() cellTemplate: TemplateRef<any>;
@@ -77,6 +79,7 @@ export class GridColumnComponent implements OnInit {
         col.datePlusTime = this.datePlusTime;
         col.ternaryBool = this.ternaryBool;
         col.timezoneContextOrg = this.timezoneContextOrg;
+        col.dateOnlyIntervalField = this.dateOnlyIntervalField;
         col.isAuto = false;
         this.grid.context.columnSet.add(col);
 
index 612a6a8..23e23ec 100644 (file)
@@ -30,6 +30,7 @@ export class GridColumn {
     ternaryBool: boolean;
     timezoneContextOrg: number;
     cellTemplate: TemplateRef<any>;
+    dateOnlyIntervalField: string;
 
     cellContext: any;
     isIndex: boolean;
@@ -795,13 +796,29 @@ export class GridContext {
             return val;
         }
 
+        // Get the value of
+        let interval;
+        const intField = col.dateOnlyIntervalField;
+        if (intField) {
+            if (intField in row) {
+                interval = this.getObjectFieldValue(row, intField);
+            } else  {
+                // find the referenced column
+                const intCol = this.columnSet.columns.filter(c => c.path === intField)[0];
+                if (intCol) {
+                    interval = this.nestedItemFieldValue(row, intCol);
+                }
+            }
+        }
+
         return this.format.transform({
             value: val,
             idlClass: col.idlClass,
             idlField: col.idlFieldDef ? col.idlFieldDef.name : col.name,
             datatype: col.datatype,
             datePlusTime: Boolean(col.datePlusTime),
-            timezoneContextOrg: Number(col.timezoneContextOrg)
+            timezoneContextOrg: Number(col.timezoneContextOrg),
+            dateOnlyInterval: interval
         });
     }
 
index 2fdc7bc..6127891 100644 (file)
     </div>
   </div>
 </div>
+
+
+<div class="mt-4 mb-4">
+  <h4>Due Date Pipe Examples</h4>
+  <div class="row">
+    <div class="col-lg-2">Due Date Daily Duration:</div>
+    <div class="col-lg-2">{{circDaily | egDueDate}}</div>
+    <div class="col-lg-2">Due Date Hourly Duration:</div>
+    <div class="col-lg-2">{{circHourly | egDueDate}}</div>
+  </div>
+</div>
index c88c010..edc35ac 100644 (file)
@@ -117,6 +117,9 @@ export class SandboxComponent implements OnInit {
     aLocation: IdlObject; // acpl
     orgClassCallback: (orgId: number) => string;
 
+    circDaily: IdlObject;
+    circHourly: IdlObject;
+
     constructor(
         private idl: IdlService,
         private org: OrgService,
@@ -328,6 +331,18 @@ export class SandboxComponent implements OnInit {
 
         const str = 'C&#xe9;sar&nbsp;&amp;&nbsp;Me';
         console.log(this.h2txt.htmlToTxt(str));
+
+        const org =
+            this.org.list().filter(o => o.ou_type().can_have_vols() === 't')[0];
+        this.circDaily = this.idl.create('circ');
+        this.circDaily.duration('1 day');
+        this.circDaily.due_date(new Date().toISOString());
+        this.circDaily.circ_lib(org.id());
+
+        this.circHourly = this.idl.create('circ');
+        this.circHourly.duration('1 hour');
+        this.circHourly.due_date(new Date().toISOString());
+        this.circHourly.circ_lib(org.id());
     }
 
     sbChannelHandler = msg => {