diff --git a/package-lock.json b/package-lock.json index 1ac54acd..0ab28ced 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "maska": "^2.1.1", "pinia": "^2.1.4", "pinia-plugin-persistedstate": "^2.2.0", + "uuid": "^9.0.1", "vee-validate": "^4.5.7", "vue": "^3.3.4", "vue-plugin-load-script": "^2.1.0", @@ -11599,6 +11600,15 @@ "node": ">=10" } }, + "node_modules/jest-junit/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/jest-leak-detector": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz", @@ -16662,6 +16672,15 @@ "websocket-driver": "^0.7.4" } }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -17730,10 +17749,13 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } diff --git a/package.json b/package.json index b196ea43..82ca9450 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "maska": "^2.1.1", "pinia": "^2.1.4", "pinia-plugin-persistedstate": "^2.2.0", + "uuid": "^9.0.1", "vee-validate": "^4.5.7", "vue": "^3.3.4", "vue-plugin-load-script": "^2.1.0", diff --git a/src/assets/img/icons/Google.svg b/src/assets/img/icons/Google.svg new file mode 100644 index 00000000..606351ec --- /dev/null +++ b/src/assets/img/icons/Google.svg @@ -0,0 +1,14 @@ + diff --git a/src/assets/img/icons/Outlook.com.svg b/src/assets/img/icons/Outlook.com.svg new file mode 100644 index 00000000..467b585a --- /dev/null +++ b/src/assets/img/icons/Outlook.com.svg @@ -0,0 +1,16 @@ + diff --git a/src/assets/img/icons/Outlook.svg b/src/assets/img/icons/Outlook.svg new file mode 100644 index 00000000..f974fb13 --- /dev/null +++ b/src/assets/img/icons/Outlook.svg @@ -0,0 +1,16 @@ + diff --git a/src/assets/img/icons/Yahoo.svg b/src/assets/img/icons/Yahoo.svg new file mode 100644 index 00000000..0206eccb --- /dev/null +++ b/src/assets/img/icons/Yahoo.svg @@ -0,0 +1,13 @@ + diff --git a/src/assets/img/icons/add-to-calendar.svg b/src/assets/img/icons/add-to-calendar.svg new file mode 100644 index 00000000..fd621c5c --- /dev/null +++ b/src/assets/img/icons/add-to-calendar.svg @@ -0,0 +1,11 @@ + diff --git a/src/assets/img/icons/iCal.svg b/src/assets/img/icons/iCal.svg new file mode 100644 index 00000000..399bcdea --- /dev/null +++ b/src/assets/img/icons/iCal.svg @@ -0,0 +1,11 @@ + diff --git a/src/constants/application-config.js b/src/constants/application-config.js index ce39577b..7230f485 100644 --- a/src/constants/application-config.js +++ b/src/constants/application-config.js @@ -19,7 +19,11 @@ const applicationConfig = Object.freeze({ ISS_DEV_CMS_DOMAIN: 'https://digitalisscms.dev.safelite.io', CASH_PARENT_ACCOUNT_NUMBER: 167132, ITAC_FAIR_AND_REASONABLE_BILLTO: '214616', // Lib Mutal FnR BillTo - ITAC_CASH_BILLTO: '283393' // Lib Mutal ITAC Cash BillTo + ITAC_CASH_BILLTO: '283393', // Lib Mutal ITAC Cash BillTo + GOOGLE_CALENDAR: 'https://www.google.com/calendar/render?action=TEMPLATE', + YAHOO_CALENDAR: 'https://calendar.yahoo.com/?v=60', + OUTLOOK_CALENDAR: + 'https://outlook.office.com/calendar/deeplink/compose?path=/calendar/action/compose&rru=addevent' }); export default applicationConfig; diff --git a/src/constants/calendar-options.js b/src/constants/calendar-options.js new file mode 100644 index 00000000..22e9d5ef --- /dev/null +++ b/src/constants/calendar-options.js @@ -0,0 +1,8 @@ +const calendarOptions = Object.freeze({ + ICAL: 'iCal', + GOOGLE: 'Google', + OUTLOOK: 'Outlook', + OUTLOOKCOM: 'Outlook.com', + YAHOO: 'Yahoo' +}); +export default calendarOptions; diff --git a/src/constants/calendar-status.js b/src/constants/calendar-status.js new file mode 100644 index 00000000..5174f33a --- /dev/null +++ b/src/constants/calendar-status.js @@ -0,0 +1,7 @@ +const calendarStatus = Object.freeze({ + FREE: 'Free', + BUSY: 'Busy', + TENTATIVE: 'Tentative', + OUT_OF_THE_OFFICE: 'OutOfTheOffice' +}); +export default calendarStatus; diff --git a/src/constants/service-type.js b/src/constants/service-type.js new file mode 100644 index 00000000..ebbec62d --- /dev/null +++ b/src/constants/service-type.js @@ -0,0 +1,6 @@ +const serviceType = Object.freeze({ + REPLACEMENT: 'replacement', + REPAIR: 'repair', + REPLACEMENT_AND_RECALIBRATION: 'replacement and recalibration' +}); +export default serviceType; diff --git a/src/helpers/add-to-calendar-helper.js b/src/helpers/add-to-calendar-helper.js new file mode 100644 index 00000000..b00f891b --- /dev/null +++ b/src/helpers/add-to-calendar-helper.js @@ -0,0 +1,75 @@ +import calendarStatus from '@/constants/calendar-status'; +import { getDateFormat } from '@/helpers/date-helper'; + +function ToCalendarFileString(str, replacementArg = '
') { + return str.replace('\r\n', replacementArg); +} + +export function getCalendarFile(calFile) { + const dateFormat = 'yyyyMMddTHHmmss'; + const calEvent = []; + calEvent.push('BEGIN:VCALENDAR'); + calEvent.push('VERSION:2.0'); + calEvent.push('BEGIN:VEVENT'); + calEvent.push(`DTSTAMP:${calFile.TimeStamp}`); + calEvent.push(`UID:${calFile.UniqueId}@safelite.com`); + calEvent.push('PRODID:noreply@safelite.com'); + switch (calFile.Status) { + case calendarStatus.BUSY: + calEvent.push('X-MICROSOFT-CDO-BUSYSTATUS:BUSY'); + break; + case calendarStatus.FREE: + calEvent.push('TRANSP:TRANSPARENT'); + break; + case calendarStatus.TENTATIVE: + calEvent.push('STATUS:TENTATIVE'); + break; + case calendarStatus.OUT_OF_THE_OFFICE: + calEvent.push('X-MICROSOFT-CDO-BUSYSTATUS:OOF'); + break; + default: + // throw new Exception("Invalid CalendarStatus"); + } + if (calFile.AllDayEvent) { + calEvent.push(`DTSTART;VALUE=DATE:${getDateFormat(calFile.StartDate, dateFormat)}`); + calEvent.push(`DTEND;;VALUE=DATE:${getDateFormat(calFile.EndDate, dateFormat)}`); + } else { + calEvent.push(`DTSTART:${getDateFormat(calFile.StartDate, dateFormat)}`); + calEvent.push(`DTEND:${getDateFormat(calFile.EndDate, dateFormat)}`); + } + + calEvent.push(`SUMMARY:${ToCalendarFileString(calFile.Subject, ' ')}`); + if (calFile.Location != null && calFile.Location !== '') { + calEvent.push(`LOCATION:${ToCalendarFileString(calFile.Location, ' ')}`); + } + if (calFile.Body != null && calFile.Body !== '') { + if (calFile.IsHTML) { + calEvent.push(`DESCRIPTION:${ToCalendarFileString(calFile.Body)}`); + calEvent.push('X-ALT-DESC;FMTTYPE=text/html:' + + '' + + '
'
+ + `${
+ ToCalendarFileString(calFile.Body)
+ }`);
+ } else {
+ calEvent.push(`DESCRIPTION:${ToCalendarFileString(calFile.Body)}`);
+ }
+ }
+ calEvent.push('END:VEVENT');
+ calEvent.push('END:VCALENDAR');
+ return calEvent.join('\r\n');
+}
+
+export function download(filename, fileBody) {
+ const element = document.createElement('a');
+ element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(fileBody)}`);
+ element.setAttribute('download', filename);
+
+ element.style.display = 'none';
+ document.body.appendChild(element);
+
+ element.click();
+
+ document.body.removeChild(element);
+}
diff --git a/src/helpers/date-helper.js b/src/helpers/date-helper.js
index be8161e8..70f598a6 100644
--- a/src/helpers/date-helper.js
+++ b/src/helpers/date-helper.js
@@ -104,3 +104,72 @@ export function get12HourTimeFormat(time) {
}
return timeString.join(''); // return adjusted time or original string
}
+
+export function getDateFormat(date, format) {
+ if (date instanceof Date !== true) return;
+ const year = date.getFullYear();
+ const month = (`0${date.getMonth() + 1}`).slice(-2);
+ const day = (`0${date.getDate()}`).slice(-2);
+ const hour = (`0${date.getHours()}`).slice(-2);
+ const minute = (`0${date.getMinutes()}`).slice(-2);
+ const second = (`0${date.getSeconds()}`).slice(-2);
+ // eslint-disable-next-line consistent-return
+ return format
+ .replace('yyyy', year)
+ .replace('MM', month)
+ .replace('dd', day)
+ .replace('hh', hour)
+ .replace('HH', hour)
+ .replace('mm', minute)
+ .replace('ss', second);
+}
+
+export function padTo2Digits(time) {
+ // Use the built-in method toString() with a radix of 10 to convert the time value to a decimal string
+ // eslint-disable-next-line no-param-reassign
+ time = time.toString(10);
+ // Use the conditional operator to check if the length of the string is less than 2
+ return time.length < 2
+ // If yes, prepend a '0' to the string and return it
+ // If no, return the original string
+ ? `0${time}` : time;
+}
+
+export function convertMsToTime(milliseconds) {
+ let seconds = Math.floor(milliseconds / 1000);
+ let minutes = Math.floor(seconds / 60);
+ const hours = Math.floor(minutes / 60);
+
+ seconds %= 60;
+ minutes %= 60;
+ // commenting to get 24 time format
+ // hours = hours % 24;
+
+ return `${padTo2Digits(hours)}${padTo2Digits(minutes)}`;
+}
+
+export function calculateDuration(startDate, endDate) {
+ if (startDate instanceof Date !== true) return;
+ if (endDate instanceof Date !== true) return;
+ // eslint-disable-next-line consistent-return
+ return convertMsToTime(endDate - startDate);
+}
+
+export function combineDateAndTime(date, time) {
+ // Use the Date.parse() method to convert the date and time strings to a numeric value
+ const timestamp = Date.parse(`${date}T${time}`);
+ // Use the new Date() constructor to create a new date object from the numeric value
+ const newDate = new Date(timestamp);
+ // Return the new date object
+ return newDate;
+}
+export function addMinutes(date, minutes) {
+ return new Date(date.getTime() + minutes * 60000);
+}
+export function shortTimeString(date) {
+ // Use a ternary operator to check if the input is a valid date object
+ return date instanceof Date
+ // Use the built-in method toLocaleTimeString() to get the short time string in the current locale
+ // Return undefined if the input is not a valid date object
+ ? date.toLocaleTimeString('en-us', { hour: 'numeric', minute: 'numeric', hour12: true }) : undefined;
+}
diff --git a/src/layouts/order-confirmation/add-to-calendar/add-to-calendar.spec.js b/src/layouts/order-confirmation/add-to-calendar/add-to-calendar.spec.js
new file mode 100644
index 00000000..7efff67b
--- /dev/null
+++ b/src/layouts/order-confirmation/add-to-calendar/add-to-calendar.spec.js
@@ -0,0 +1,187 @@
+// Components
+import calendarOptions from '@/constants/calendar-options';
+
+// Supporting Files
+import { shallowMount } from '@vue/test-utils';
+import { getMountOptions } from '@/helpers/unit-test-helper.js';
+import { useMainStore } from '@/store';
+import addToCalendar from '@/layouts/order-confirmation/add-to-calendar/add-to-calendar.vue';
+
+const appointmentText = 'Appointment content';
+function setupMocks({ customMountOptions }) {
+ const mountOptions = getMountOptions({
+ ...customMountOptions
+ });
+ mountOptions.mixins = [
+ {
+ methods: {
+ getCmsContent: jest.fn().mockImplementation(() => appointmentText)
+ }
+ }
+ ];
+ const wrapper = shallowMount(addToCalendar, mountOptions);
+ wrapper.vm.$refs.calendarModalQuestion.openModal = jest.fn();
+ return { wrapper };
+}
+
+beforeEach(() => {
+ jest.restoreAllMocks();
+ jest.clearAllMocks();
+ useMainStore().submittedOrder = {
+ schedule: {
+ date: '2019-01-01',
+ startTime: '09:00',
+ endTime: '10:00',
+ routeCode: '000'
+ },
+ lineItems: {
+ glassParts: [
+ {
+ partNumber: 'ABC123'
+ }
+ ],
+ supportingItems: []
+ },
+ serviceLocation: {
+ address: '',
+ address2: null,
+ city: '',
+ state: 'AZ',
+ appointmentType: 'Inshop',
+ zipCode: '12345',
+ zipCodeCtu: '01234',
+ provider: {
+ providerNumber: '123',
+ address: {
+ streetAddress: 'test1',
+ city: 'test',
+ state: 'AZ',
+ zipCode: '12345',
+ zipCodeCtu: '01234'
+ }
+ }
+ },
+ damage: {
+ isRepair: false
+ },
+ referralNumber: '1234567',
+ payment: {
+ isInsurance: true
+ }
+ };
+});
+afterEach(() => {
+ useMainStore().submittedOrder = {};
+ jest.restoreAllMocks();
+ jest.clearAllMocks();
+});
+
+describe('Add-to-calendar methods...', () => {
+ test('Add-to-calendar should trigger openModal method', () => {
+ // Arrange
+
+ const { wrapper } = setupMocks({
+ customMountOptions: {
+ propsData: {
+ appointment: {
+ RefrrlSeqNum: '123456',
+ StartDate: new Date(2023, 9, 11, 12, 0, 0),
+ EndDate: new Date(2023, 9, 11, 13, 0, 0),
+ Subject: 'Meeting with John',
+ Location: 'Conference Room',
+ Body: 'Discuss the project progress',
+ IsHTML: true,
+ Duration: '1220'
+ }
+ }
+ }
+ });
+
+ // Act
+ wrapper.vm.openCalendarModal();
+
+ // Assert
+ expect(wrapper.vm.$refs.calendarModalQuestion.openModal).toBeCalled();
+ });
+
+ test('getCalendarData should return expected model value.', () => {
+ // Arrange
+ const type = calendarOptions.OUTLOOKCOM;
+ const { wrapper } = setupMocks({
+ customMountOptions: {
+ propsData: {
+ appointment: {
+ RefrrlSeqNum: '123456',
+ StartDate: new Date(2023, 9, 11, 12, 0, 0),
+ EndDate: new Date(2023, 9, 11, 13, 0, 0),
+ Subject: 'Meeting with John',
+ Location: 'Conference Room',
+ Body: 'Discuss the project progress',
+ IsHTML: true,
+ Duration: '1220'
+ }
+ }
+ }
+ });
+
+ // Act
+ const testValue = wrapper.vm.getCalendarData(type);
+
+ // Assert
+ expect(testValue.name).toEqual(calendarOptions.OUTLOOKCOM);
+ });
+
+ test('setCalendarAppointment should call getAppointment method for iCAL and Outlook.', () => {
+ // Arrange
+ const newValue = { name: calendarOptions.OUTLOOK };
+ const { wrapper } = setupMocks({
+ customMountOptions: {
+ propsData: {
+ appointment: {
+ RefrrlSeqNum: '123456',
+ StartDate: new Date(2023, 9, 11, 12, 0, 0),
+ EndDate: new Date(2023, 9, 11, 13, 0, 0),
+ Subject: 'Meeting with John',
+ Location: 'Conference Room',
+ Body: 'Discuss the project progress',
+ IsHTML: true,
+ Duration: '1220'
+ }
+ }
+ }
+ });
+ wrapper.vm.getAppointment = jest.fn();
+ // Act
+ wrapper.vm.setCalendarAppointment(newValue);
+
+ // Assert
+ expect(wrapper.vm.getAppointment).toHaveBeenCalled();
+ });
+ test('setCalendarAppointment should call window.open for Google, yahoo and Outlook.com.', () => {
+ // Arrange
+ const newValue = { name: calendarOptions.GOOGLE };
+ const { wrapper } = setupMocks({
+ customMountOptions: {
+ propsData: {
+ appointment: {
+ RefrrlSeqNum: '123456',
+ StartDate: new Date(2023, 9, 11, 12, 0, 0),
+ EndDate: new Date(2023, 9, 11, 13, 0, 0),
+ Subject: 'Meeting with John',
+ Location: 'Conference Room',
+ Body: 'Discuss the project progress',
+ IsHTML: true,
+ Duration: '1220'
+ }
+ }
+ }
+ });
+ window.open = jest.fn();
+
+ // Act
+ wrapper.vm.setCalendarAppointment(newValue);
+
+ // Assert
+ expect(window.open).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/layouts/order-confirmation/add-to-calendar/add-to-calendar.vue b/src/layouts/order-confirmation/add-to-calendar/add-to-calendar.vue
new file mode 100644
index 00000000..ebd7eb47
--- /dev/null
+++ b/src/layouts/order-confirmation/add-to-calendar/add-to-calendar.vue
@@ -0,0 +1,325 @@
+
+
{{ appointmentDateFormatted }}
{{ appointmentTimeFormatted }}
+