Add-to-calendar
This commit is contained in:
parent
7afbc56a3a
commit
03a6e2d994
16 changed files with 936 additions and 20 deletions
14
src/assets/img/icons/Google.svg
Normal file
14
src/assets/img/icons/Google.svg
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g fill-rule="nonzero">
|
||||
<g>
|
||||
<g>
|
||||
<path fill="#FBBB00" d="M4.432 12.086l-.696 2.6-2.544.053C.432 13.329 0 11.715 0 10c0-1.658.403-3.222 1.118-4.599l2.266.415.992 2.252c-.207.606-.32 1.256-.32 1.932 0 .734.132 1.437.376 2.086z" transform="translate(-119 -800) translate(104 753) translate(15 47)"/>
|
||||
<path fill="#518EF8" d="M19.825 8.132C19.94 8.737 20 9.362 20 10c0 .716-.075 1.414-.219 2.088-.486 2.292-1.758 4.294-3.52 5.71h-.001l-2.853-.146-.404-2.52c1.169-.686 2.083-1.76 2.564-3.044H10.22V8.132h9.605z" transform="translate(-119 -800) translate(104 753) translate(15 47)"/>
|
||||
<path fill="#28B446" d="M16.26 17.798C14.547 19.176 12.37 20 10 20c-3.808 0-7.12-2.129-8.808-5.261l3.24-2.653c.845 2.254 3.02 3.859 5.568 3.859 1.096 0 2.122-.297 3.003-.814l3.257 2.667z" transform="translate(-119 -800) translate(104 753) translate(15 47)"/>
|
||||
<path fill="#F14336" d="M16.383 2.302l-3.24 2.652c-.911-.57-1.989-.899-3.143-.899-2.607 0-4.821 1.678-5.624 4.013L1.12 5.401C2.781 2.192 6.134 0 10 0c2.426 0 4.651.864 6.383 2.302z" transform="translate(-119 -800) translate(104 753) translate(15 47)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
16
src/assets/img/icons/Outlook.com.svg
Normal file
16
src/assets/img/icons/Outlook.com.svg
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="20" viewBox="0 0 22 20">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g fill="#0072C6" fill-rule="nonzero">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M12.794 4.525v3.677l1.37.809c.036.01.114.01.15 0l5.899-3.728c0-.441-.44-.758-.687-.758h-6.732z" transform="translate(-119 -870) translate(104 753) translate(15 117)"/>
|
||||
<path d="M12.794 9.574l1.25.805c.176.121.389 0 .389 0-.212.121 5.78-3.61 5.78-3.61v6.757c0 .736-.502 1.044-1.067 1.044h-6.353V9.574z" transform="translate(-119 -870) translate(104 753) translate(15 117)"/>
|
||||
<g>
|
||||
<path d="M6.266 7.736c-.427 0-.766.187-1.017.562-.25.375-.376.871-.376 1.489 0 .627.125 1.122.376 1.487.25.365.58.546.986.546.42 0 .753-.177.998-.532.246-.354.37-.846.37-1.475 0-.656-.12-1.166-.359-1.531-.238-.364-.564-.546-.978-.546z" transform="translate(-119 -870) translate(104 753) translate(15 117) translate(.052 .143)"/>
|
||||
<path d="M.332 2.515v14.383l11.671 2.295V.397L.333 2.515zm7.81 9.623c-.493.609-1.136.914-1.93.914-.773 0-1.403-.295-1.89-.885-.486-.59-.73-1.358-.73-2.305 0-1 .247-1.81.741-2.427.495-.618 1.15-.927 1.966-.927.77 0 1.393.295 1.869.887.476.591.713 1.37.713 2.338.001.995-.246 1.797-.739 2.405z" transform="translate(-119 -870) translate(104 753) translate(15 117) translate(.052 .143)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
16
src/assets/img/icons/Outlook.svg
Normal file
16
src/assets/img/icons/Outlook.svg
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="20" viewBox="0 0 21 20">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g fill="#DBA702" fill-rule="nonzero">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M12.394 4.525v3.677l1.327.809c.035.01.111.01.146 0l5.714-3.728c0-.441-.425-.758-.665-.758h-6.522z" transform="translate(-119 -835) translate(104 753) translate(15 82)"/>
|
||||
<path d="M12.394 9.574l1.211.805c.171.121.377 0 .377 0-.205.121 5.6-3.61 5.6-3.61v6.757c0 .736-.487 1.044-1.034 1.044h-6.155V9.574z" transform="translate(-119 -835) translate(104 753) translate(15 82)"/>
|
||||
<g>
|
||||
<path d="M6.07 7.736c-.413 0-.742.187-.985.562-.243.375-.365.871-.365 1.489 0 .627.122 1.122.365 1.487s.562.546.956.546c.406 0 .728-.177.966-.532.238-.354.358-.846.358-1.475 0-.656-.116-1.166-.347-1.531-.231-.364-.547-.546-.948-.546z" transform="translate(-119 -835) translate(104 753) translate(15 82) translate(.05 .143)"/>
|
||||
<path d="M.322 2.515v14.383l11.306 2.295V.397L.322 2.515zm7.566 9.623c-.478.609-1.101.914-1.87.914-.75 0-1.359-.295-1.83-.885-.472-.59-.709-1.358-.709-2.305 0-1 .24-1.81.719-2.427.48-.618 1.114-.927 1.904-.927.746 0 1.35.295 1.81.887.461.591.692 1.37.692 2.338 0 .995-.239 1.797-.716 2.405z" transform="translate(-119 -835) translate(104 753) translate(15 82) translate(.05 .143)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
13
src/assets/img/icons/Yahoo.svg
Normal file
13
src/assets/img/icons/Yahoo.svg
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="20" viewBox="0 0 22 20">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g fill="#5F01D1" fill-rule="nonzero">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M0 4.868h4.29l2.498 6.39 2.53-6.39h4.177l-6.29 15.129H3.003l1.722-4.01L0 4.869zm18.358 5.106H13.68L17.832 0h4.661l-4.135 9.974zm-3.452.955c1.435 0 2.599 1.163 2.599 2.599 0 1.435-1.164 2.598-2.6 2.598-1.434 0-2.598-1.163-2.598-2.598 0-1.436 1.164-2.6 2.599-2.6z" transform="translate(-119 -905) translate(104 753) translate(15 152) translate(0 .11)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 708 B |
11
src/assets/img/icons/iCal.svg
Normal file
11
src/assets/img/icons/iCal.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="17" height="20" viewBox="0 0 17 20">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g fill="#606060" fill-rule="nonzero">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M5.44 20C2.463 19.983 0 13.907 0 10.812c0-5.055 3.793-6.161 5.254-6.161.659 0 1.362.258 1.982.487.434.16.883.325 1.132.325.15 0 .501-.14.812-.264.663-.263 1.487-.591 2.447-.591h.006c.717 0 2.89.157 4.198 2.12l.306.46-.44.332c-.63.475-1.778 1.341-1.778 3.058 0 2.032 1.3 2.814 1.925 3.19.276.166.561.337.561.71 0 .245-1.95 5.492-4.782 5.492-.692 0-1.182-.209-1.614-.392-.437-.186-.815-.346-1.438-.346-.316 0-.715.149-1.138.307-.578.216-1.232.461-1.974.461h-.02zM11.927 0C12.001 2.66 10.1 4.504 8.2 4.388 7.887 2.266 10.1 0 11.927 0z" transform="translate(-119 -763) translate(104 753) translate(15 10)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 907 B |
8
src/constants/calendar-options.js
Normal file
8
src/constants/calendar-options.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
const calendarOptions = {
|
||||
ICAL: "iCal",
|
||||
GOOGLE: "Google",
|
||||
OUTLOOK: "Outlook",
|
||||
OUTLOOKCOM: "Outlook.com",
|
||||
YAHOO: "Yahoo",
|
||||
};
|
||||
export { calendarOptions };
|
||||
7
src/constants/calendar-status.js
Normal file
7
src/constants/calendar-status.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
const calendarStatus = {
|
||||
FREE: "Free",
|
||||
BUSY: "Busy",
|
||||
TENTATIVE: "Tentative",
|
||||
OUT_OF_THE_OFFICE: "OutOfTheOffice",
|
||||
};
|
||||
export { calendarStatus };
|
||||
76
src/helpers/add-to-calendar-helper.js
Normal file
76
src/helpers/add-to-calendar-helper.js
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { calendarStatus } from "@/constants/calendar-status";
|
||||
import { getDateFormat } from "@/helpers/date-helper";
|
||||
export function getCalendarFile(calFile) {
|
||||
const dateFormat = "yyyyMMddTHHmmss";
|
||||
var calEvent = [];
|
||||
calEvent.push("BEGIN:VCALENDAR");
|
||||
calEvent.push("VERSION:2.0");
|
||||
calEvent.push("BEGIN:VEVENT");
|
||||
calEvent.push("DTSTAMP:" + getDateFormat(new Date(), dateFormat));
|
||||
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:" +
|
||||
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">' +
|
||||
"<HTML><HEAD><TITLE></TITLE></HEAD><BODY>" +
|
||||
'<P DIR=LTR><SPAN LANG="en-us">' +
|
||||
"</SPAN>" +
|
||||
ToCalendarFileString(calFile.Body) +
|
||||
"</BODY></HTML>"
|
||||
);
|
||||
} else {
|
||||
calEvent.push("DESCRIPTION:" + ToCalendarFileString(calFile.Body));
|
||||
}
|
||||
}
|
||||
calEvent.push("END:VEVENT");
|
||||
calEvent.push("END:VCALENDAR");
|
||||
return calEvent.join("\r\n");
|
||||
}
|
||||
|
||||
function ToCalendarFileString(str, replacementArg = "<p>") {
|
||||
return str.replace("\r\n", replacementArg);
|
||||
}
|
||||
|
||||
export function download(filename, fileBody) {
|
||||
var 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);
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ export function getDateDifferenceInDays(startDate, endDate) {
|
|||
// To calculate the no. of days between two dates
|
||||
return Difference_In_Time / (1000 * 3600 * 24);
|
||||
}
|
||||
|
||||
export function getFullDayName(date) {
|
||||
if (date instanceof Date !== true) return;
|
||||
const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||
|
|
@ -62,3 +61,67 @@ export function get12HourTimeMobileFormat(time) {
|
|||
}
|
||||
return time.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);
|
||||
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 calculateDuration(startDate, endDate) {
|
||||
if (startDate instanceof Date !== true) return;
|
||||
if (endDate instanceof Date !== true) return;
|
||||
return convertMsToTime(endDate - startDate);
|
||||
}
|
||||
export function convertMsToTime(milliseconds) {
|
||||
let seconds = Math.floor(milliseconds / 1000);
|
||||
let minutes = Math.floor(seconds / 60);
|
||||
let hours = Math.floor(minutes / 60);
|
||||
|
||||
seconds = seconds % 60;
|
||||
minutes = minutes % 60;
|
||||
//commenting to get 24 time format
|
||||
//hours = hours % 24;
|
||||
|
||||
return `${padTo2Digits(hours)}${padTo2Digits(minutes)}`;
|
||||
}
|
||||
export function padTo2Digits(time) {
|
||||
return ("0" + time).slice(-2);
|
||||
}
|
||||
export function combineDateAndTime(date, time) {
|
||||
var newDate = new Date(date);
|
||||
var timeParts = time.split(":");
|
||||
newDate.setHours(timeParts[0]);
|
||||
newDate.setMinutes(timeParts[1]);
|
||||
return newDate;
|
||||
}
|
||||
export function addMinutes(date, minutes) {
|
||||
return new Date(date.getTime() + minutes * 60000);
|
||||
}
|
||||
export function shortTimeString(date) {
|
||||
if (date instanceof Date !== true) return;
|
||||
const hour = ("0" + date.getHours()).slice(-2);
|
||||
const minute = ("0" + date.getMinutes()).slice(-2);
|
||||
//const second = ("0" + date.getSeconds()).slice(-2);
|
||||
let time = hour + ":" + minute;
|
||||
// Check correct time format and split into components
|
||||
time = time.toString().match(/^([01]\d|2[0-3])(:)([0-5]\d)?$/) || [time];
|
||||
|
||||
if (time.length > 1) {
|
||||
// If time format correct
|
||||
time = time.slice(1); // Remove full string match value
|
||||
time[5] = +time[0] < 12 ? " AM" : " PM"; // Set AM/PM
|
||||
time[0] = +time[0] % 12 || 12; // Adjust hours
|
||||
}
|
||||
return time.join(""); // return adjusted time or original string
|
||||
}
|
||||
|
|
|
|||
94
src/layouts/add-to-calendar/add-to-calendar.spec.js
Normal file
94
src/layouts/add-to-calendar/add-to-calendar.spec.js
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// Components
|
||||
import addToCalendar from "@/layouts/add-to-calendar/add-to-calendar.vue";
|
||||
|
||||
// Supporting Files
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
import store from "@/store";
|
||||
|
||||
beforeEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
jest.clearAllMocks();
|
||||
store.getters = {
|
||||
order: {
|
||||
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,
|
||||
},
|
||||
lineItems: {
|
||||
glassParts: [],
|
||||
supportingItems: [],
|
||||
},
|
||||
};
|
||||
});
|
||||
afterEach(() => {
|
||||
store.getters = {};
|
||||
jest.restoreAllMocks();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Add-to-calendar methods...", () => {
|
||||
test("Add-to-calendar should trigger openModal method", () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
//Act
|
||||
wrapper.vm.openCalendarModal();
|
||||
|
||||
//Assert
|
||||
expect(wrapper.vm.$refs.calendarModalQuestion.openModal).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
function setupMocks({ customMountOptions }) {
|
||||
const mountOptions = getMountOptions({
|
||||
...customMountOptions,
|
||||
});
|
||||
|
||||
const wrapper = shallowMount(addToCalendar, mountOptions);
|
||||
|
||||
//wrapper.vm.setCmsContent = jest.fn();
|
||||
//wrapper.vm.$refs.datePicker.initializeComponent = jest.fn();
|
||||
//wrapper.vm.$refs.locationAlerts.initializeComponent = jest.fn();
|
||||
//wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
|
||||
wrapper.vm.$refs.calendarModalQuestion.openModal = jest.fn();
|
||||
|
||||
return { wrapper };
|
||||
}
|
||||
300
src/layouts/add-to-calendar/add-to-calendar.vue
Normal file
300
src/layouts/add-to-calendar/add-to-calendar.vue
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
<template>
|
||||
<div class="calendar-section">
|
||||
<div class="add-to-calendar">
|
||||
<button v-on:click="openCalendarModal">
|
||||
<span class="add-to-calendar-span">
|
||||
<img src="@/assets/img/icons/add-to-calendar.svg" class="add-to-calendar-img" />
|
||||
<span class="add-to-calendar-text">Add to calendar</span></span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<calendarModalQuestion
|
||||
ref="calendarModalQuestion"
|
||||
customComponentId="calendarModalQuestion"
|
||||
cmsWidgetName="CalendarModalQuestion"
|
||||
@calendar-modal-closed="calendarModalClosed"
|
||||
:calendarOptionsData="calendarValues"
|
||||
v-model="selectedCalendarOption">
|
||||
</calendarModalQuestion>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { calendarOptions } from "@/constants/calendar-options";
|
||||
import { calendarStatus } from "@/constants/calendar-status";
|
||||
import calendarModalQuestion from "@/layouts/add-to-calendar/calendar-modal-question/calendar-modal-question";
|
||||
import {
|
||||
getDateFormat,
|
||||
calculateDuration,
|
||||
combineDateAndTime,
|
||||
shortTimeString,
|
||||
addMinutes,
|
||||
} from "@/helpers/date-helper";
|
||||
import { getCalendarFile, download } from "@/helpers/add-to-calendar-helper";
|
||||
import { AppointmentTypeStrings, RouteCodeFlags } from "@/constants/schedule-constants";
|
||||
import store from "@/store";
|
||||
|
||||
export default {
|
||||
name: "add-to-calendar",
|
||||
components: {
|
||||
calendarModalQuestion,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
appointment: this.getappointmentData(),
|
||||
selectedCalendarOption: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
calendarValues: {
|
||||
get: function () {
|
||||
var obj = [];
|
||||
const options = Object.values(calendarOptions);
|
||||
for (var i = 0; i < options.length; i++) {
|
||||
obj[i] = this.getCalendarData(options[i]);
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selectedCalendarOption(newValue) {
|
||||
if (newValue) {
|
||||
this.setCalendarAppointment(newValue);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
setCalendarAppointment(calendarOption) {
|
||||
if (
|
||||
calendarOption.name.includes(calendarOptions.ICAL) ||
|
||||
calendarOption.name.includes(calendarOptions.OUTLOOK)
|
||||
) {
|
||||
this.getAppointment();
|
||||
} else {
|
||||
window.open(calendarOption.url);
|
||||
}
|
||||
},
|
||||
calendarModalClosed() {
|
||||
// Clear the selectedOption if close the modal
|
||||
this.selectedCalendarOption = null;
|
||||
},
|
||||
openCalendarModal() {
|
||||
this.$refs["calendarModalQuestion"].openModal();
|
||||
},
|
||||
scheduleDate() {
|
||||
return store.getters.order.schedule.date;
|
||||
},
|
||||
scheduleStartTime() {
|
||||
return store.getters.order.schedule.startTime;
|
||||
},
|
||||
scheduleEndTime() {
|
||||
return store.getters.order.schedule.endTime;
|
||||
},
|
||||
serviceLocationAddress() {
|
||||
return store.getters.order.serviceLocation.address;
|
||||
},
|
||||
ServiceLocationAddress2() {
|
||||
return store.getters.order.serviceLocation.address2;
|
||||
},
|
||||
serviceLocationCity() {
|
||||
return store.getters.order.serviceLocation.city;
|
||||
},
|
||||
serviceLocationState() {
|
||||
return store.getters.order.serviceLocation.state;
|
||||
},
|
||||
serviceLocationZipCode() {
|
||||
return store.getters.order.serviceLocation.zipCode;
|
||||
},
|
||||
providerAddress() {
|
||||
return store.getters.order.serviceLocation.provider.address.streetAddress;
|
||||
},
|
||||
providerCity() {
|
||||
return store.getters.order.serviceLocation.provider.address.city;
|
||||
},
|
||||
providerState() {
|
||||
return store.getters.order.serviceLocation.provider.address.state;
|
||||
},
|
||||
providerZipCode() {
|
||||
return store.getters.order.serviceLocation.provider.address.zipCode;
|
||||
},
|
||||
appointmentType() {
|
||||
return store.getters.order.serviceLocation.appointmentType;
|
||||
},
|
||||
uniqueId() {
|
||||
return store.getters.order.referralNumber?.toString();
|
||||
},
|
||||
serviceType() {
|
||||
const isRepair = store.getters.order.damage.isRepair;
|
||||
const funnelHasRecalibrationPart = store.getters.funnelHasRecalibrationPart;
|
||||
if (isRepair) {
|
||||
if (funnelHasRecalibrationPart) {
|
||||
return "replacement and recalibration";
|
||||
} else {
|
||||
return "replacement";
|
||||
}
|
||||
}
|
||||
return "repair";
|
||||
},
|
||||
routeCode() {
|
||||
return store.getters.order.schedule.routeCode;
|
||||
},
|
||||
isSameDayDropOff() {
|
||||
const todaysDate = new Date().toISOString().split("T")[0];
|
||||
return this.scheduleDate() === todaysDate;
|
||||
},
|
||||
getCalendarData(type) {
|
||||
var URL = "";
|
||||
const dateFormat = "yyyyMMddTHHmmss";
|
||||
const outlookComTimeSpanFormat = "yyyy-MM-ddTHH:mm:ss";
|
||||
if (type == calendarOptions.OUTLOOKCOM) {
|
||||
URL = `https://outlook.office.com/calendar/deeplink/compose?path=/calendar/action/compose&rru=addevent&startdt=${encodeURIComponent(
|
||||
getDateFormat(this.appointment.StartDate, outlookComTimeSpanFormat)
|
||||
)}&enddt=${encodeURIComponent(
|
||||
getDateFormat(this.appointment.EndDate, outlookComTimeSpanFormat)
|
||||
)}&subject=${encodeURIComponent(
|
||||
this.appointment.Subject
|
||||
)}&body=${encodeURIComponent(this.appointment.Body)}&location=${encodeURIComponent(
|
||||
this.appointment.Location
|
||||
)}`;
|
||||
}
|
||||
if (type == calendarOptions.GOOGLE) {
|
||||
URL = `https://www.google.com/calendar/render?action=TEMPLATE&text=${encodeURIComponent(
|
||||
this.appointment.Subject
|
||||
)}&dates=${encodeURIComponent(
|
||||
getDateFormat(this.appointment.StartDate, dateFormat)
|
||||
)}/${encodeURIComponent(
|
||||
getDateFormat(this.appointment.EndDate, dateFormat)
|
||||
)}&details=${encodeURIComponent(
|
||||
this.appointment.Body
|
||||
)}&location=${encodeURIComponent(this.appointment.Location)}&sf=true&output=xml`;
|
||||
}
|
||||
if (type == calendarOptions.YAHOO) {
|
||||
URL = `https://calendar.yahoo.com/?v=60&TITLE=${encodeURIComponent(
|
||||
this.appointment.Subject
|
||||
)}&DESC=${encodeURIComponent(this.appointment.Body)}&ST=${encodeURIComponent(
|
||||
getDateFormat(this.appointment.StartDate, dateFormat)
|
||||
)}&DUR=${this.appointment.Duration}&in_loc=${encodeURIComponent(
|
||||
this.appointment.Location
|
||||
)}`;
|
||||
}
|
||||
return {
|
||||
url: URL,
|
||||
name: type,
|
||||
icon: require(`@/assets/img/icons/${type}.svg`),
|
||||
};
|
||||
},
|
||||
getappointmentData() {
|
||||
var subject = "";
|
||||
var location = "";
|
||||
var startDateTime = combineDateAndTime(this.scheduleDate(), this.scheduleStartTime());
|
||||
var endDateTime = combineDateAndTime(this.scheduleDate(), this.scheduleEndTime());
|
||||
var body = "";
|
||||
var isHTML = false;
|
||||
var duration = "";
|
||||
|
||||
if (this.appointmentType() === AppointmentTypeStrings.MOBILE) {
|
||||
location = `${
|
||||
this.ServiceLocationAddress2() ? this.ServiceLocationAddress2() + ", " : ""
|
||||
}${this.serviceLocationAddress()}, ${this.serviceLocationCity()}, ${this.serviceLocationState()} ${this.serviceLocationZipCode()}`;
|
||||
subject = `Safelite ${this.serviceType()} appointment`;
|
||||
|
||||
body =
|
||||
"Your safety is our top priority, so we'll let you know who's coming to you and your tech will reach out to let you know when they're on the way.";
|
||||
} else {
|
||||
location = `${this.providerAddress()}, ${this.providerCity()}, ${this.providerState()} ${this.providerZipCode()}`;
|
||||
if (this.appointmentType() === AppointmentTypeStrings.DROP_OFF) {
|
||||
if (this.routeCode().includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
|
||||
subject = "Drop off your vehicle by 5:30 PM";
|
||||
|
||||
body =
|
||||
"Drop off your vehicle by 5:30 PM the night of your scheduled appointment. The shop will work with you on a convenient pick-up time.";
|
||||
} else if (this.routeCode().includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
|
||||
if (this.isSameDayDropOff()) {
|
||||
subject = "Drop off your vehicle in the next 30 - 60 minutes";
|
||||
endDateTime = addMinutes(startDateTime, 120);
|
||||
body =
|
||||
"Drop your vehicle off in the next 30 - 60 minutes. We'll work your vehicle into our day's schedule. The shop will work with you on a convenient pick-up time.";
|
||||
} else {
|
||||
endDateTime = addMinutes(startDateTime, 120);
|
||||
subject =
|
||||
"Drop off your vehicle between " +
|
||||
shortTimeString(startDateTime) +
|
||||
" - " +
|
||||
shortTimeString(endDateTime);
|
||||
|
||||
body = `Drop off your vehicle by ${shortTimeString(
|
||||
endDateTime
|
||||
)} and pick it up later in the day. When you arrive, park in an open space and follow the key drop instructions. We'll text you when your vehicle is ready.`;
|
||||
}
|
||||
} //normal time slot
|
||||
else {
|
||||
subject = `Safelite ${this.serviceType()} appointment`;
|
||||
body = "Just a reminder that you're coming to us at " + location + ".";
|
||||
}
|
||||
} else {
|
||||
subject = `Safelite ${this.serviceType()} appointment`;
|
||||
body = "Just a reminder that you're coming to us at " + location + ".";
|
||||
}
|
||||
duration = calculateDuration(startDateTime, endDateTime);
|
||||
}
|
||||
return {
|
||||
Subject: subject,
|
||||
Location: location,
|
||||
StartDate: startDateTime,
|
||||
EndDate: endDateTime,
|
||||
RefrrlSeqNum: this.uniqueId(),
|
||||
Body: body,
|
||||
IsHTML: isHTML,
|
||||
Duration: duration,
|
||||
};
|
||||
},
|
||||
|
||||
getAppointment() {
|
||||
let body = this.appointment.Body;
|
||||
body = body.replace("|", "<").replace("~", ">");
|
||||
|
||||
const calFile = {
|
||||
Location: this.appointment.Location,
|
||||
Body: body,
|
||||
IsHTML: this.appointment.IsHTML,
|
||||
UniqueId: this.appointment.RefrrlSeqNum,
|
||||
Subject: this.appointment.Subject,
|
||||
StartDate: this.appointment.StartDate,
|
||||
EndDate: this.appointment.EndDate,
|
||||
Status: calendarStatus.BUSY,
|
||||
};
|
||||
var calBody = getCalendarFile(calFile);
|
||||
download("SafeliteAppointment.ics", calBody);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.add-to-calendar-text {
|
||||
margin: 0 0 0 7px;
|
||||
color: #1574a1;
|
||||
vertical-align: middle;
|
||||
font-weight: 500;
|
||||
font-family: "Roboto";
|
||||
}
|
||||
.add-to-calendar button {
|
||||
border: 0;
|
||||
background-color: #fff;
|
||||
}
|
||||
.calendar-section {
|
||||
display: table;
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.calendar-section .add-to-calendar {
|
||||
padding-top: 15px;
|
||||
}
|
||||
.calendar-section .add-to-calendar .add-to-calendar-span {
|
||||
cursor: pointer;
|
||||
}
|
||||
.calendar-section .add-to-calendar .add-to-calendar-img {
|
||||
display: inline;
|
||||
width: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
<template>
|
||||
<baseInputButton
|
||||
v-bind="$props"
|
||||
buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 mb-2"
|
||||
v-model="selectedValue">
|
||||
<div
|
||||
:aria-label="buttonLabel"
|
||||
class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4">
|
||||
<span class="m-0 position-relative" :class="textPosition">
|
||||
<img :src="buttonLabel" />
|
||||
<span class="calendar-text">{{ value }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</baseInputButton>
|
||||
</template>
|
||||
<script>
|
||||
import baseInputButton from "@/digital-components/base-input-button/base-input-button";
|
||||
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
|
||||
|
||||
export default {
|
||||
name: "calendarModalListButton",
|
||||
mixins: [inputButtonWrapperMixin],
|
||||
components: {
|
||||
baseInputButton,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.list-button {
|
||||
outline: none;
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
position: static; //override bootstrap
|
||||
|
||||
&:focus-visible + .list-button-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
&:focus + .list-button-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
&:checked + .list-button-content {
|
||||
color: $black;
|
||||
font-weight: 500;
|
||||
background: $blue-100;
|
||||
box-shadow: 0 0 0 1px $blue;
|
||||
}
|
||||
&:checked:focus + .list-button-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
&:checked + .list-button-content p,
|
||||
&:checked + .list-button-content span {
|
||||
font-weight: 500;
|
||||
}
|
||||
&:checked + .list-button-content span:nth-child(2) {
|
||||
font-weight: 400;
|
||||
color: $gray-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
.list-button-content {
|
||||
color: $gray-600;
|
||||
position: relative;
|
||||
background: $white;
|
||||
transition: all 150ms linear;
|
||||
border-radius: $border-radius-lg;
|
||||
border: 1px solid $gray-500;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
|
||||
span.calendar-text {
|
||||
margin-left: 5px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.position-relative {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
import calendarModalQuestion from "@/layouts/add-to-calendar/calendar-modal-question/calendar-modal-question.vue";
|
||||
|
||||
describe("calendar-modal-question.vue", () => {
|
||||
test("When selected calendar option is changed, a correctly selectedCalendarOption should be emitted", async () => {
|
||||
//Arrange
|
||||
const url = "iCal url";
|
||||
const name = "iCal";
|
||||
const icon = "iCal.svg";
|
||||
const expectedEmit = [
|
||||
[
|
||||
{
|
||||
url: url,
|
||||
name: name,
|
||||
icon: icon,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
customMountOptions: {
|
||||
propsData: {
|
||||
calendarOptionsData: [
|
||||
{
|
||||
url: url,
|
||||
name: name,
|
||||
icon: icon,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.vm.$refs.calendarOptions.closeModal = jest.fn();
|
||||
wrapper.vm.selectedCalendarOption = name;
|
||||
|
||||
//Act
|
||||
wrapper.vm.setSelectedCalendarOption();
|
||||
|
||||
//Assert
|
||||
expect(wrapper.emitted()["update:modelValue"]).toEqual(expectedEmit);
|
||||
});
|
||||
});
|
||||
|
||||
function setupMocks({ customMountOptions }) {
|
||||
const mountOptions = getMountOptions({
|
||||
...customMountOptions,
|
||||
});
|
||||
|
||||
const wrapper = shallowMount(calendarModalQuestion, mountOptions);
|
||||
return { wrapper };
|
||||
}
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
<template>
|
||||
<modal
|
||||
:ref="modalName"
|
||||
:fontSize="true"
|
||||
:headerText="modalHeaderText"
|
||||
:footerButtonText="footerCloseButtonText"
|
||||
:onModalClosedCallback="onModalClosed"
|
||||
@isModalOpened="setModalStatus"
|
||||
@footer-button-event="setSelectedCalendarOption"
|
||||
class="calendar-modal">
|
||||
<template v-if="isModalOpened">
|
||||
<buttonQuestion
|
||||
ref="buttonQuestion"
|
||||
buttonTypeString="calendarModalListButton"
|
||||
:buttonTypeObject="calendarModalListButton"
|
||||
:answers="getCalendarOptionData"
|
||||
groupName="chooseCalendarType"
|
||||
textPosition="text-center"
|
||||
v-model="selectedCalendarOption"
|
||||
isRequired
|
||||
validationRules="calendar-option-required" />
|
||||
</template>
|
||||
</modal>
|
||||
</template>
|
||||
<script>
|
||||
import modal from "@/digital-components/modal/modal";
|
||||
import buttonQuestion from "@/digital-components/button-question/button-question";
|
||||
import calendarModalListButton from "./calendar-modal-list-button/calendar-modal-list-button";
|
||||
import { defineRule, useField } from "vee-validate";
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
import { required } from "@/helpers/validation-rules";
|
||||
import { deepClone } from "@/helpers/object-helper";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// Validation for the modal button
|
||||
defineRule("calendar-option-required", required(errorMessages.OPTION_REQUIRED));
|
||||
export default {
|
||||
name: "calendar-modal-question",
|
||||
components: {
|
||||
modal,
|
||||
buttonQuestion,
|
||||
},
|
||||
props: {
|
||||
calendarOptionsData: Object,
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
url: null,
|
||||
name: null,
|
||||
icon: null,
|
||||
}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isModalOpened: false,
|
||||
selectedValue: null,
|
||||
calendarModalListButton: calendarModalListButton,
|
||||
};
|
||||
},
|
||||
setup(props) {
|
||||
const uuid = uuidv4();
|
||||
const componentId = !props.customComponentId
|
||||
? `component-${uuid}`
|
||||
: props.customComponentId;
|
||||
|
||||
const modelValue = deepClone(props).modelValue;
|
||||
const initialValue = modelValue;
|
||||
|
||||
const fieldOptions = {
|
||||
value: modelValue,
|
||||
initialValue: initialValue,
|
||||
};
|
||||
|
||||
const { errorMessage, handleChange, meta, validate, errors } = useField(
|
||||
componentId,
|
||||
props.validationRules,
|
||||
fieldOptions
|
||||
);
|
||||
|
||||
return {
|
||||
componentId,
|
||||
errorMessage,
|
||||
handleChange,
|
||||
validate,
|
||||
meta,
|
||||
errors,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
modelValue: {
|
||||
handler(newValue) {
|
||||
this.handleChange(newValue);
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
modalName() {
|
||||
return "calendarOptions";
|
||||
},
|
||||
modalHeaderText() {
|
||||
return "Add to Calendar";
|
||||
},
|
||||
footerCloseButtonText() {
|
||||
return "Add to Calendar";
|
||||
},
|
||||
modal() {
|
||||
return this.$refs[this.modalName];
|
||||
},
|
||||
selectedCalendarOption: {
|
||||
get: function () {
|
||||
return this.modelValue?.name;
|
||||
},
|
||||
set: function (newValue) {
|
||||
// Button Question only supports Number, or String data types so we must convert to the full object before emitting
|
||||
this.selectedValue = this.getSelectedCalendarObject(newValue);
|
||||
},
|
||||
},
|
||||
getCalendarOptionData() {
|
||||
return this.calendarOptionsData.map((item) => {
|
||||
return {
|
||||
value: item.name,
|
||||
buttonLabel: item.icon,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openModal() {
|
||||
this.modal.openModal();
|
||||
},
|
||||
setModalStatus(isOpened) {
|
||||
this.isModalOpened = isOpened;
|
||||
},
|
||||
closeModal() {
|
||||
this.modal.closeModal();
|
||||
},
|
||||
onModalClosed() {
|
||||
this.$emit("calendar-modal-closed");
|
||||
},
|
||||
async setSelectedCalendarOption() {
|
||||
this.$emit("update:modelValue", this.selectedValue);
|
||||
this.closeModal();
|
||||
},
|
||||
|
||||
getSelectedCalendarObject(calendarOptionId) {
|
||||
const calendarOption = this.calendarOptionsData?.find(
|
||||
(calendarOption) => calendarOption.name == calendarOptionId
|
||||
);
|
||||
|
||||
if (calendarOption) {
|
||||
return {
|
||||
url: calendarOption.url,
|
||||
name: calendarOption.name,
|
||||
icon: calendarOption.icon,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
url: null,
|
||||
name: null,
|
||||
icon: null,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.calendar-modal.modal.modal-component {
|
||||
> .modal-dialog > .modal-content {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.modal-header {
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -20,21 +20,7 @@
|
|||
<p>{{ this.getScheduleDate() }}</p>
|
||||
<p>{{ this.getScheduleTime() }}</p>
|
||||
</div>
|
||||
|
||||
<div class="calendar-section">
|
||||
<div class="add-to-calendar">
|
||||
<button>
|
||||
<span class="add-to-calendar-span">
|
||||
<img
|
||||
src="~@/assets/img/icons/add-to-calendar.svg"
|
||||
class="add-to-calendar-img" />
|
||||
<span class="add-to-calendar-text"
|
||||
>Add to calendar</span
|
||||
></span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<addToCalendar></addToCalendar>
|
||||
<div
|
||||
class="appoinmenttext"
|
||||
v-html="this.getAppointmentWordingText()"></div>
|
||||
|
|
@ -57,6 +43,7 @@
|
|||
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
|
||||
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
|
||||
import navbar from "@/fmg-components/nav-bar/nav-bar";
|
||||
import addToCalendar from "@/layouts/add-to-calendar/add-to-calendar";
|
||||
//Supporting files
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
|
||||
|
|
@ -192,7 +179,7 @@ export default {
|
|||
|
||||
// Schedule
|
||||
const schedule = store.getters.order.schedule;
|
||||
const scheduleReqs = !!(schedule.date && schedule.startTime); //removing schedule.endTime for testing
|
||||
const scheduleReqs = !!(schedule.date && schedule.startTime && schedule.endTime);
|
||||
|
||||
return serviceLocationReqs && scheduleReqs;
|
||||
},
|
||||
|
|
@ -245,6 +232,7 @@ export default {
|
|||
funnelHeader,
|
||||
navbar,
|
||||
vehicleBanner,
|
||||
addToCalendar,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -191,9 +191,9 @@ export default {
|
|||
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
|
||||
},
|
||||
async forwardButtonAction() {
|
||||
this.dispatchStoreAction(storeActions.SAVE_PIA_ERROR_CODE, null, false);
|
||||
this.dispatchStoreAction(this.storeActions.SAVE_WORK_ORDER_FLAG, true, false);
|
||||
await submitWorkOrder({ pageNameToLog: "payment-method" });
|
||||
// this.dispatchStoreAction(storeActions.SAVE_PIA_ERROR_CODE, null, false);
|
||||
// this.dispatchStoreAction(this.storeActions.SAVE_WORK_ORDER_FLAG, true, false);
|
||||
// await submitWorkOrder({ pageNameToLog: "payment-method" });
|
||||
this.$router.navigateWithoutSaving(
|
||||
this.navigationScenarios.CLICKED_FORWARD,
|
||||
this.$route
|
||||
|
|
|
|||
Loading…
Reference in a new issue