From bca84f2fce0ddfc177fac36b10f15aa474bd0aaf Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Tue, 29 Jun 2021 13:50:56 -0400 Subject: [PATCH] LP1934164 egDueDate and egOrgDateInContext Angular pipes 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 Signed-off-by: Jane Sandberg --- Open-ILS/src/eg2/src/app/core/core.module.ts | 10 ++- Open-ILS/src/eg2/src/app/core/format.service.ts | 93 +++++++++++++++++++++- .../src/app/share/grid/grid-column.component.ts | 3 + Open-ILS/src/eg2/src/app/share/grid/grid.ts | 19 ++++- .../src/app/staff/sandbox/sandbox.component.html | 11 +++ .../eg2/src/app/staff/sandbox/sandbox.component.ts | 15 ++++ 6 files changed, 144 insertions(+), 7 deletions(-) diff --git a/Open-ILS/src/eg2/src/app/core/core.module.ts b/Open-ILS/src/eg2/src/app/core/core.module.ts index 40cfe29dc4..431ca1428e 100644 --- a/Open-ILS/src/eg2/src/app/core/core.module.ts +++ b/Open-ILS/src/eg2/src/app/core/core.module.ts @@ -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, diff --git a/Open-ILS/src/eg2/src/app/core/format.service.ts b/Open-ILS/src/eg2/src/app/core/format.service.ts index 9622bd0681..b73209b2d7 100644 --- a/Open-ILS/src/eg2/src/app/core/format.service.ts +++ b/Open-ILS/src/eg2/src/app/core/format.service.ts @@ -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() + }); + } +} + diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts index 38220605c5..f3651f3687 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts +++ b/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts @@ -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; @@ -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); diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.ts index 612a6a874d..23e23ec500 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid.ts +++ b/Open-ILS/src/eg2/src/app/share/grid/grid.ts @@ -30,6 +30,7 @@ export class GridColumn { ternaryBool: boolean; timezoneContextOrg: number; cellTemplate: TemplateRef; + 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 }); } diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html index 2fdc7bc65c..6127891a16 100644 --- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html +++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html @@ -455,3 +455,14 @@ + + +
+

Due Date Pipe Examples

+
+
Due Date Daily Duration:
+
{{circDaily | egDueDate}}
+
Due Date Hourly Duration:
+
{{circHourly | egDueDate}}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts index c88c010876..edc35acde9 100644 --- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts @@ -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ésar & 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 => { -- 2.11.0