Add timezone to profile settings page

 - Users will have two options to select a timezone.
 - UTC and browser offset timezone are the two options for the application.
 - date-fns and date-fns-tz is used for date and time manipulations because:-
   - The package size of library is smaller.
   - It allows for importing functions to work with the native date object
     rather than having to create a moment instance that carries a larger payload.

Signed-off-by: Sukanya Pandey <sukapan1@in.ibm.com>
Change-Id: I581803f230f501c0d34d0b53e7c2d89e8466ee60
diff --git a/package-lock.json b/package-lock.json
index 03fb44f..98709b2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6004,10 +6004,14 @@
       }
     },
     "date-fns": {
-      "version": "1.30.1",
-      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz",
-      "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==",
-      "dev": true
+      "version": "2.14.0",
+      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.14.0.tgz",
+      "integrity": "sha512-1zD+68jhFgDIM0rF05rcwYO8cExdNqxjq4xP1QKM60Q45mnO6zaMWB4tOzrIr4M4GSLntsKeE4c9Bdl2jhL/yw=="
+    },
+    "date-fns-tz": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-1.0.10.tgz",
+      "integrity": "sha512-cHQAz0/9uDABaUNDM80Mj1FL4ODlxs1xEY4b0DQuAooO2UdNKvDkNbV8ogLnxLbv02Ru1HXFcot0pVvDRBgptg=="
     },
     "de-indent": {
       "version": "1.0.2",
@@ -11802,6 +11806,14 @@
         "cli-cursor": "^2.1.0",
         "date-fns": "^1.27.2",
         "figures": "^2.0.0"
+      },
+      "dependencies": {
+        "date-fns": {
+          "version": "1.30.1",
+          "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz",
+          "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==",
+          "dev": true
+        }
       }
     },
     "load-json-file": {
diff --git a/package.json b/package.json
index 115937b..ae92065 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,8 @@
     "bootstrap": "4.4.1",
     "bootstrap-vue": "2.12.0",
     "core-js": "3.3.2",
+    "date-fns": "2.14.0",
+    "date-fns-tz": "1.0.10",
     "js-cookie": "2.2.1",
     "lodash": "4.17.19",
     "vue": "2.6.11",
diff --git a/src/locales/en-US.json b/src/locales/en-US.json
index 023efa5..63c7536 100644
--- a/src/locales/en-US.json
+++ b/src/locales/en-US.json
@@ -367,14 +367,22 @@
       "solConsole": "Serial over LAN console"
     }
   },
-  "profileSettings": {
+  "pageProfileSettings": {
+    "browserOffset": "Browser offset (%{timezone})",
     "changePassword": "Change password",
     "confirmPassword": "Confirm new password",
+    "defaultUTC": "Default (UTC)",
     "newPassword": "New password",
     "newPassLabelTextInfo": "Password must be between %{min} - %{max} characters",
     "passwordsDoNotMatch": "Passwords do not match",
     "profileInfoTitle": "Profile information",
-    "username": "Username"
+    "timezone": "Timezone",
+    "timezoneDisplay": "Timezone display preference",
+    "timezoneDisplayDesc": "Select how time is displayed throughout the application",
+    "username": "Username",
+    "toast": {
+      "successSaveSettings": "Successfully saved account settings."
+    }
   },
   "pageManagePowerUsage": {
     "description": "Set a power cap to keep power consumption at or below the specified value in watts",
diff --git a/src/main.js b/src/main.js
index 0c6d5b0..8336cb3 100644
--- a/src/main.js
+++ b/src/main.js
@@ -33,23 +33,46 @@
 } from 'bootstrap-vue';
 import Vuelidate from 'vuelidate';
 import i18n from './i18n';
+import { format } from 'date-fns-tz';
 
 // Filters
+Vue.filter('shortTimeZone', function(value) {
+  const longTZ = value
+    .toString()
+    .match(/\((.*)\)/)
+    .pop();
+  const regexNotUpper = /[*a-z ]/g;
+  return longTZ.replace(regexNotUpper, '');
+});
+
 Vue.filter('formatDate', function(value) {
+  const isUtcDisplay = store.getters['global/isUtcDisplay'];
+
   if (value instanceof Date) {
-    return value.toISOString().substring(0, 10);
+    if (isUtcDisplay) {
+      return value.toISOString().substring(0, 10);
+    }
+    const pattern = `yyyy-MM-dd`;
+    const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+    return format(value, pattern, { timezone });
   }
 });
 
 Vue.filter('formatTime', function(value) {
-  const timeOptions = {
-    hour: 'numeric',
-    minute: 'numeric',
-    second: 'numeric',
-    timeZoneName: 'short'
-  };
+  const isUtcDisplay = store.getters['global/isUtcDisplay'];
+
   if (value instanceof Date) {
-    return value.toLocaleTimeString('default', timeOptions);
+    if (isUtcDisplay) {
+      let timeOptions = {
+        timeZone: 'UTC',
+        hour12: false
+      };
+      return `${value.toLocaleTimeString('default', timeOptions)} UTC`;
+    }
+    const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+    const shortTz = Vue.filter('shortTimeZone')(value);
+    const pattern = `HH:mm:ss ('${shortTz}' O)`;
+    return format(value, pattern, { timezone }).replace('GMT', 'UTC');
   }
 });
 
diff --git a/src/store/modules/GlobalStore.js b/src/store/modules/GlobalStore.js
index 55b0796..39f3d1d 100644
--- a/src/store/modules/GlobalStore.js
+++ b/src/store/modules/GlobalStore.js
@@ -32,12 +32,16 @@
     bmcTime: null,
     hostStatus: 'unreachable',
     languagePreference: localStorage.getItem('storedLanguage') || 'en-US',
+    isUtcDisplay: localStorage.getItem('storedUtcDisplay')
+      ? JSON.parse(localStorage.getItem('storedUtcDisplay'))
+      : true,
     username: localStorage.getItem('storedUsername')
   },
   getters: {
     hostStatus: state => state.hostStatus,
     bmcTime: state => state.bmcTime,
     languagePreference: state => state.languagePreference,
+    isUtcDisplay: state => state.isUtcDisplay,
     username: state => state.username
   },
   mutations: {
@@ -46,7 +50,8 @@
       (state.hostStatus = hostStateMapper(hostState)),
     setLanguagePreference: (state, language) =>
       (state.languagePreference = language),
-    setUsername: (state, username) => (state.username = username)
+    setUsername: (state, username) => (state.username = username),
+    setUtcTime: (state, isUtcDisplay) => (state.isUtcDisplay = isUtcDisplay)
   },
   actions: {
     async getBmcTime({ commit }) {
diff --git a/src/views/ProfileSettings/ProfileSettings.vue b/src/views/ProfileSettings/ProfileSettings.vue
index 07aac19..2abee63 100644
--- a/src/views/ProfileSettings/ProfileSettings.vue
+++ b/src/views/ProfileSettings/ProfileSettings.vue
@@ -4,9 +4,11 @@
 
     <b-row>
       <b-col md="8" lg="8" xl="6">
-        <page-section :section-title="$t('profileSettings.profileInfoTitle')">
+        <page-section
+          :section-title="$t('pageProfileSettings.profileInfoTitle')"
+        >
           <dl>
-            <dt>{{ $t('profileSettings.username') }}</dt>
+            <dt>{{ $t('pageProfileSettings.username') }}</dt>
             <dd>
               {{ username }}
             </dd>
@@ -18,10 +20,12 @@
     <b-form @submit.prevent="submitForm">
       <b-row>
         <b-col sm="8" md="6" xl="3">
-          <page-section :section-title="$t('profileSettings.changePassword')">
+          <page-section
+            :section-title="$t('pageProfileSettings.changePassword')"
+          >
             <b-form-group
               id="input-group-1"
-              :label="$t('profileSettings.newPassword')"
+              :label="$t('pageProfileSettings.newPassword')"
               label-for="input-1"
             >
               <b-form-text id="password-help-block">
@@ -42,9 +46,6 @@
                   @input="$v.form.newPassword.$touch()"
                 />
                 <b-form-invalid-feedback role="alert">
-                  <template v-if="!$v.form.newPassword.required">
-                    {{ $t('global.form.fieldRequired') }}
-                  </template>
                   <template
                     v-if="
                       !$v.form.newPassword.minLength ||
@@ -52,7 +53,7 @@
                     "
                   >
                     {{
-                      $t('profileSettings.newPassLabelTextInfo', {
+                      $t('pageProfileSettings.newPassLabelTextInfo', {
                         min: passwordRequirements.minLength,
                         max: passwordRequirements.maxLength
                       })
@@ -63,7 +64,7 @@
             </b-form-group>
             <b-form-group
               id="input-group-2"
-              :label="$t('profileSettings.confirmPassword')"
+              :label="$t('pageProfileSettings.confirmPassword')"
               label-for="input-2"
             >
               <input-password-toggle>
@@ -75,11 +76,8 @@
                   @input="$v.form.confirmPassword.$touch()"
                 />
                 <b-form-invalid-feedback role="alert">
-                  <template v-if="!$v.form.confirmPassword.required">
-                    {{ $t('global.form.fieldRequired') }}
-                  </template>
-                  <template v-else-if="!$v.form.confirmPassword.sameAsPassword">
-                    {{ $t('profileSettings.passwordsDoNotMatch') }}
+                  <template v-if="!$v.form.confirmPassword.sameAsPassword">
+                    {{ $t('pageProfileSettings.passwordsDoNotMatch') }}
                   </template>
                 </b-form-invalid-feedback>
               </input-password-toggle>
@@ -87,16 +85,45 @@
           </page-section>
         </b-col>
       </b-row>
+      <page-section :section-title="$t('pageProfileSettings.timezoneDisplay')">
+        <p>{{ $t('pageProfileSettings.timezoneDisplayDesc') }}</p>
+        <b-row>
+          <b-col md="9" lg="8" xl="9">
+            <b-form-group :label="$t('pageProfileSettings.timezone')">
+              <b-form-radio
+                v-model="form.isUtcDisplay"
+                :value="true"
+                @change="$v.form.isUtcDisplay.$touch()"
+              >
+                {{ $t('pageProfileSettings.defaultUTC') }}
+              </b-form-radio>
+              <b-form-radio
+                v-model="form.isUtcDisplay"
+                :value="false"
+                @change="$v.form.isUtcDisplay.$touch()"
+              >
+                {{
+                  $t('pageProfileSettings.browserOffset', {
+                    timezone
+                  })
+                }}
+              </b-form-radio>
+            </b-form-group>
+          </b-col>
+        </b-row>
+      </page-section>
       <b-button variant="primary" type="submit">
-        {{ $t('global.action.save') }}
+        {{ $t('global.action.saveSettings') }}
       </b-button>
     </b-form>
   </b-container>
 </template>
 
 <script>
+import i18n from '@/i18n';
 import BVToastMixin from '@/components/Mixins/BVToastMixin';
 import InputPasswordToggle from '@/components/Global/InputPasswordToggle';
+import { format } from 'date-fns-tz';
 import {
   maxLength,
   minLength,
@@ -116,7 +143,8 @@
     return {
       form: {
         newPassword: '',
-        confirmPassword: ''
+        confirmPassword: '',
+        isUtcDisplay: this.$store.getters['global/isUtcDisplay']
       }
     };
   },
@@ -126,6 +154,12 @@
     },
     passwordRequirements() {
       return this.$store.getters['localUsers/accountPasswordRequirements'];
+    },
+    timezone() {
+      const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+      const shortTz = this.$options.filters.shortTimeZone(new Date());
+      const pattern = `'${shortTz}' O`;
+      return format(new Date(), pattern, { timezone }).replace('GMT', 'UTC');
     }
   },
   created() {
@@ -137,21 +171,21 @@
   validations() {
     return {
       form: {
+        isUtcDisplay: { required },
         newPassword: {
-          required,
           minLength: minLength(this.passwordRequirements.minLength),
           maxLength: maxLength(this.passwordRequirements.maxLength)
         },
         confirmPassword: {
-          required,
           sameAsPassword: sameAs('newPassword')
         }
       }
     };
   },
   methods: {
-    submitForm() {
-      this.$v.$touch();
+    saveNewPasswordInputData() {
+      this.$v.form.confirmPassword.$touch();
+      this.$v.form.newPassword.$touch();
       if (this.$v.$invalid) return;
       let userData = {
         originalUsername: this.username,
@@ -166,6 +200,21 @@
           this.successToast(message);
         })
         .catch(({ message }) => this.errorToast(message));
+    },
+    saveTimeZonePrefrenceData() {
+      localStorage.setItem('storedUtcDisplay', this.form.isUtcDisplay);
+      this.$store.commit('global/setUtcTime', this.form.isUtcDisplay);
+      this.successToast(
+        i18n.t('pageProfileSettings.toast.successSaveSettings')
+      );
+    },
+    submitForm() {
+      if (this.form.confirmPassword || this.form.newPassword) {
+        this.saveNewPasswordInputData();
+      }
+      if (this.$v.form.isUtcDisplay.$anyDirty) {
+        this.saveTimeZonePrefrenceData();
+      }
     }
   }
 };