LP1816475: Reservations grid now respects the pickup library's tz
authorJane Sandberg <sandbej@linnbenton.edu>
Sat, 30 Mar 2019 01:00:43 +0000 (18:00 -0700)
committerJane Sandberg <sandbej@linnbenton.edu>
Wed, 17 Apr 2019 20:22:00 +0000 (13:22 -0700)
Signed-off-by: Jane Sandberg <sandbej@linnbenton.edu>
Open-ILS/src/eg2/src/app/core/format.service.ts
Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.html
Open-ILS/src/eg2/src/app/share/datetime-select/datetime-select.component.ts
Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html
Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts
Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.html
Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts

index c764e61..f236769 100644 (file)
@@ -137,14 +137,47 @@ export class FormatService {
     /**
      * Create an IDL-friendly display version of a human-readable date
      */
-    idlFormatDate(original: string): string {
-        return Moment(original, 'MMM DD YYYY', 'America/Los_Angeles').format('YYYY-MM-DD');
-    }
+    idlFormatDate(date: string): string { return this.momentizeDateString(date).format('YYYY-MM-DD'); }
+
     /**
      * Create an IDL-friendly display version of a human-readable datetime
      */
-    idlFormatDatetime(original: string): string {
-        return Moment(original, 'MMM DD YYYY HH:mm', 'America/Los_Angeles').toISOString()
+    idlFormatDatetime(datetime: string): string { return this.momentizeDateTimeString(datetime).toISOString(); }
+
+    momentizeDateString(date: string): Moment {
+        const parseableFormat = this.makeFormatParseable(this.dateFormat);
+        if (parseableFormat.length) {return Moment(date, parseableFormat, 'Asia/Tokyo')};
+        return Moment(date, 'Asia/Tokyo');
+    }
+
+    momentizeDateTimeString(datetime: string): Moment {
+        const parseableFormat = this.makeFormatParseable(this.dateTimeFormat);
+        if (parseableFormat.length) {return Moment(datetime, parseableFormat, 'Asia/Tokyo')};
+        return Moment(datetime, 'Asia/Tokyo');
+    }
+
+    /**
+     * Takes a dateFormate or dateTimeFormat string (which uses Angular syntax) and transforms
+     * it into a format string that MomentJs can use to parse input human-readable strings
+     * (https://momentjs.com/docs/#/parsing/string-format/)
+     *
+     * Returns a blank string if it can't do this transformation.
+     */
+    private makeFormatParseable(original: string): string {
+        const specialFormats = ['short', 'medium', 'long', 'full',
+            'shortDate', 'mediumDate', 'longDate', 'fullDate',
+            'shortTime', 'mediumTime', 'longTime', 'fullTime'];
+        if (!original || specialFormats.includes(original)) { return ''; };
+        return original
+            .replace(/a+/, 'a') // MomentJs can handle all sorts of meridian strings
+            .replace('d', 'D') // MomentJs capitalizes day of month
+            .replace('EEEEEE', '') // MomentJs does not handle short day of week
+            .replace('EEEEE', '') // MomentJs does not handle narrow day of week
+            .replace('EEEE', 'dddd') // MomentJs has different syntax for long day of week
+            .replace(/E{1,3}/, 'ddd') // MomentJs has different syntax for abbreviated day of week
+            .replace('L', 'M') // MomentJs does not differentiate between month and month standalone
+            .replace('W', '') // MomentJs uses W for something else
+            .replace('y', 'Y'); // MomentJs capitalizes year
     }
 }
 
index ee00660..08ff81d 100644 (file)
@@ -1,17 +1,10 @@
-<!-- TODO:
-* Have this accept ISO input (i.e. from the database)
-* Have this accept @Input params of timezone (default to OU), disable
-* turn this into a popover that pops over the input: https://ng-bootstrap.github.io/#/components/popover/examples
-* have this input and output the five and noble stuff in the correct format (currently you can't just type ssomething into the box and have it show up on the calendar, but it does show up in the model - cool)
-
--->
 <div class="input-group">
   <input type="datetime"
     [attr.id]="domId.length ? domId : null" 
     name="{{fieldName}}"
     class="form-control"
     [placeholder]="initialIso"
-    [(ngModel)]="dateTime"
+    [(ngModel)]="stringVersion"
     (blur)="blurred($event)"
     #dtPicker="ngbPopover"
     [ngbPopover]="dt"
@@ -31,7 +24,7 @@
 <ng-template #dt>
   <ngb-datepicker
     [(ngModel)]="dateModel"
-    (ngModelChange)="modelChanged($event)"
+    (ngModelChange)="modelChanged()"
     [footerTemplate]="time">
   </ngb-datepicker>
 </ng-template>
index e9101cd..d46aed8 100644 (file)
@@ -22,7 +22,7 @@ export class DateTimeSelectComponent implements OnInit {
 
     @Output() onChangeAsIso = new EventEmitter();
 
-    dateTime: any; // Used internally on internal input
+    stringVersion: any; // Used internally on internal input
     timeModel: NgbTimeStruct;
     dateModel: NgbDateStruct;
 
@@ -59,21 +59,21 @@ export class DateTimeSelectComponent implements OnInit {
     }
 
     modelChanged(event) {
-        let newDate: any;
+        let newDate: Moment;
 
-        if (event) {
-            newDate = Moment(new Date(this.dateTime));
+        if (event) { // if the change came from the input field
+            newDate = this.format.momentizeDateTimeString(this.stringVersion);
         } else {
-            newDate = new Date(this.dateModel.year, this.dateModel.month, this.dateModel.day,
-                this.timeModel.hour, this.timeModel.minute, this.timeModel.second);
+            newDate = Moment(new Date(this.dateModel.year, (this.dateModel.month - 1), this.dateModel.day,
+                this.timeModel.hour, this.timeModel.minute, this.timeModel.second));
         }
 
         if (newDate && !isNaN(newDate)) {
             console.log('newDate');
             // Set component view value
-           this.dateTime = this.format.transform({value: newDate, datatype: 'string', datePlusTime: true});
+           this.stringVersion = this.format.transform({value: newDate, datatype: 'timestamp', datePlusTime: true});
             // Update form passed in view value
-            this.onChangeAsIso.emit(Moment(newDate).toISOString);
+            this.onChangeAsIso.emit(newDate.toISOString());
         }
     }
 
index 2331740..beaf6a7 100644 (file)
@@ -47,6 +47,8 @@
 
             <ng-container *ngSwitchCase="'timestamp-timepicker'">
               <eg-datetime-select
+                [showTZ]="timezone"
+                [timezone]="timezone"
                 domId="{{idPrefix}}-{{field.name}}"
                 (onChangeAsIso)="record[field.name]($event)"
                 initialIso="{{record[field.name]()}}">
index f78ead3..fdb880a 100644 (file)
@@ -86,6 +86,9 @@ export class FmRecordEditorComponent
     mode: 'create' | 'update' | 'view' = 'create';
     recId: any;
 
+    // Show datetime fields in this particular timezone
+    timezone: string;
+
     // IDL record we are editing
     // TODO: allow this to be update in real time by the caller?
     record: IdlObject;
@@ -111,7 +114,7 @@ export class FmRecordEditorComponent
     @Input() requiredFieldsList: string[] = [];
     @Input() requiredFields: string; // comma-separated string version
 
-    // list of timezone fields that should display with a timepicker
+    // list of timestamp fields that should display with a timepicker
     @Input() datetimeFieldsList: string[] = [];
     @Input() datetimeFields: string; // comma-separated string version
 
@@ -525,4 +528,3 @@ export class FmRecordEditorComponent
     }
 }
 
-
index 7b48afa..5d4aebf 100644 (file)
@@ -8,8 +8,8 @@
   <eg-grid-toolbar-action label="View Reservations for This Resource" i18n-label [action]="viewByResource" [disableOnRows]="notOneResourceSelected"></eg-grid-toolbar-action>
 
   <eg-grid-column name="id" [hidden]="true" [index]="true" i18n-label label="ID" path="id"></eg-grid-column>
-  <eg-grid-column label="Patron username" i18n-label path="usr.usrname"></eg-grid-column>
-  <eg-grid-column label="Patron barcode" i18n-label path="usr.card.barcode"></eg-grid-column>
+  <eg-grid-column label="Patron username" i18n-label path="usr.usrname" [sortable]="false"></eg-grid-column>
+  <eg-grid-column label="Patron barcode" i18n-label path="usr.card.barcode" [sortable]="false"></eg-grid-column>
   <eg-grid-column label="Patron first name" i18n-label  path="usr.first_given_name"></eg-grid-column>
   <eg-grid-column label="Patron middle name" i18n-label [hidden]="true" path="usr.second_given_name"></eg-grid-column>
   <eg-grid-column label="Patron family name" i18n-label path="usr.family_name"></eg-grid-column>
   <eg-grid-column name="pickup_time" label="Pickup Time" [datePlusTime]="true" i18n-label path="pickup_time" datatype="timestamp"></eg-grid-column>
   <eg-grid-column label="Email notify" i18n-label [hidden]="true" path="email_notify" datatype="bool"></eg-grid-column>
   <eg-grid-column i18n-label [hidden]="true" path="unrecovered" datatype="bool"></eg-grid-column>
-  <eg-grid-column label="Billing total" i18n-label path="billing_total" datatype="money"></eg-grid-column>
-  <eg-grid-column label="Payment total" i18n-label path="payment_total" datatype="money"></eg-grid-column>
+  <eg-grid-column label="Billing total" i18n-label path="billing_total" datatype="money" [sortable]="false"></eg-grid-column>
+  <eg-grid-column label="Payment total" i18n-label path="payment_total" datatype="money" [sortable]="false"></eg-grid-column>
   <eg-grid-column label="Booking interval" i18n-label [hidden]="true" path="booking_interval" [hidden]="true"></eg-grid-column>
   <eg-grid-column label="Fine interval" i18n-label [hidden]="true" path="fine_interval" [hidden]="true"></eg-grid-column>
   <eg-grid-column label="Fine amount" i18n-label [hidden]="true" path="fine_amount" datatype="money"></eg-grid-column>
   <eg-grid-column label="Maximum fine" i18n-label [hidden]="true" path="max_fine" datatype="money"></eg-grid-column>
   <eg-grid-column i18n-label label="Resource Barcode" path="current_resource.barcode"></eg-grid-column>
   <eg-grid-column i18n-label label="Note" path="note"></eg-grid-column>
-  <eg-grid-column i18n-label label="Resource Type" path="target_resource_type.name"></eg-grid-column>
+  <eg-grid-column i18n-label label="Resource Type" path="target_resource_type.name" [sortable]="false"></eg-grid-column>
   <eg-grid-column label="Request library" i18n-label [hidden]="true" path="request_lib.name"></eg-grid-column>
   <eg-grid-column label="Pickup library" i18n-label [hidden]="true" path="pickup_lib.name"></eg-grid-column>
+  <eg-grid-column label="Pickup library timezone" i18n-label path="timezone" [sortable]="false"></eg-grid-column>
 
 </eg-grid>
 
index 6ab727f..af5b88e 100644 (file)
@@ -15,6 +15,7 @@ import {ToastService} from '@eg/share/toast/toast.service';
 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
 import {NetService} from '@eg/core/net.service';
 import {NoTimezoneSetComponent} from './no-timezone-set.component';
+import {OrgService} from '@eg/core/org.service';
 import {PatronService} from '@eg/staff/share/patron.service';
 
 import * as Moment from 'moment-timezone';
@@ -59,13 +60,14 @@ export class ReservationsGridComponent implements OnInit {
     returnNotAppropriate: (rows: IdlObject[]) => boolean;
 
     constructor(
+        private auth: AuthService,
+        private format: FormatService,
+        private pcrud: PcrudService,
         private route: ActivatedRoute,
         private router: Router,
         private toast: ToastService,
-        private pcrud: PcrudService,
-        private auth: AuthService,
-        private format: FormatService,
         private net: NetService,
+        private org: OrgService,
         private patronService: PatronService
     ) {
 
@@ -108,7 +110,9 @@ export class ReservationsGridComponent implements OnInit {
                 flesh_fields: {'bresv' : [
                     'usr', 'capture_staff', 'target_resource', 'target_resource_type', 'current_resource', 'request_lib', 'pickup_lib'
                 ], 'au': ['card'] }
-            });
+            }).pipe(tap((row) => {
+                this.org.settings('lib.timezone', row['pickup_lib']()).then((tz) => {row['timezone'] = tz['lib.timezone']});
+            }));
         };
 
         this.editDialog.mode = 'update';
@@ -187,6 +191,7 @@ export class ReservationsGridComponent implements OnInit {
 
     showEditDialog(idlThing: IdlObject) {
         this.editDialog.recId = idlThing.id();
+        this.editDialog.timezone = idlThing['timezone'];
         return this.editDialog.open({size: 'lg'}).then(
             ok => {
                 this.toast.success('Reservation successfully updated'); // TODO: needs i18n, pluralization