Merge remote-tracking branch 'origin/develop' into feature/CASH-2752
This commit is contained in:
commit
d72eae4ea6
23 changed files with 1230 additions and 120 deletions
3
src/assets/img/icons/chevron-no-background.svg
Normal file
3
src/assets/img/icons/chevron-no-background.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.00161 3.1177C7.76595 3.11605 7.53923 3.2078 7.37109 3.37286L0.264342 10.4688C0.0950866 10.638 -1.78339e-09 10.8674 0 11.1067C1.78339e-09 11.3459 0.0950866 11.5754 0.264342 11.7446C0.433597 11.9137 0.663157 12.0088 0.90252 12.0088C1.14188 12.0088 1.37144 11.9137 1.5407 11.7446L8.00161 5.27378L14.4702 11.7446C14.6394 11.9121 14.8683 12.0055 15.1065 12.0043C15.3447 12.0031 15.5726 11.9074 15.7402 11.7382C15.9077 11.569 16.0012 11.3402 16 11.1022C15.9988 10.8641 15.903 10.6363 15.7338 10.4688L8.63468 3.37796C8.46644 3.21088 8.23878 3.11729 8.00161 3.1177Z" fill="#0070D1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 691 B |
6
src/assets/img/shop.svg
Normal file
6
src/assets/img/shop.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.348 12.5171H4.65205C3.64794 12.5171 2.87637 11.5714 3.01647 10.5128L3.33853 8.07548C3.45334 7.20759 4.15097 6.5625 4.97411 6.5625H19.0259C19.849 6.5625 20.5467 7.20759 20.6615 8.07548L20.9835 10.5128C21.1236 11.5714 20.3521 12.5171 19.348 12.5171Z" stroke="#0A0A0A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.67969 12.5156V18.5656C4.67969 19.5318 5.46294 20.315 6.42814 20.315H17.5697C18.5359 20.315 19.3182 19.5318 19.3182 18.5656V12.5156" stroke="#0A0A0A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14.1249 20.3158V17.7549C14.1249 16.5815 13.1733 15.6289 11.9989 15.6289C10.8255 15.6289 9.87305 16.5815 9.87305 17.7549V20.3158" stroke="#0A0A0A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.67969 6.55875V5.43301C4.67969 4.46684 5.46294 3.68359 6.42814 3.68359H17.5697C18.5359 3.68359 19.3182 4.46684 19.3182 5.43301V6.55875" stroke="#0A0A0A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -7,4 +7,5 @@ export const headerKeys = {
|
|||
EON: "X-Enterprise-Order-Number",
|
||||
LOG_ENABLED: "log-enabled",
|
||||
PAGE_NAME_TO_LOG: "X-Page-Name-To-Log",
|
||||
DEVICE_ID: "X-Device-Id",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -82,9 +82,6 @@ export function coverageTypeEnum(strCoverageType) {
|
|||
export const parentAccountNumbers = {
|
||||
CONNECT: "560636",
|
||||
};
|
||||
export const NON_MANAGED_SHOW_CLAIM_NUMBER_PARENTS = [
|
||||
{ name: "KentuckyFarmBureau", value: "223499" },
|
||||
];
|
||||
|
||||
export const PARENT_ACCOUNT_NUMBERS = {
|
||||
STATE_FARM: "711310",
|
||||
|
|
|
|||
|
|
@ -52,3 +52,80 @@ const stateOptions = {
|
|||
WY: "Wyoming",
|
||||
};
|
||||
export { stateOptions };
|
||||
|
||||
const stateOptionsInternational = {
|
||||
AL: "Alabama",
|
||||
AK: "Alaska",
|
||||
AZ: "Arizona",
|
||||
AR: "Arkansas",
|
||||
CA: "California",
|
||||
CO: "Colorado",
|
||||
CT: "Connecticut",
|
||||
DE: "Delaware",
|
||||
DC: "District Of Columbia",
|
||||
FL: "Florida",
|
||||
GA: "Georgia",
|
||||
GU: "Guam",
|
||||
HI: "Hawaii",
|
||||
ID: "Idaho",
|
||||
IL: "Illinois",
|
||||
IN: "Indiana",
|
||||
IA: "Iowa",
|
||||
KS: "Kansas",
|
||||
KY: "Kentucky",
|
||||
LA: "Louisiana",
|
||||
ME: "Maine",
|
||||
MD: "Maryland",
|
||||
MA: "Massachusetts",
|
||||
MI: "Michigan",
|
||||
MN: "Minnesota",
|
||||
MS: "Mississippi",
|
||||
MO: "Missouri",
|
||||
MT: "Montana",
|
||||
NE: "Nebraska",
|
||||
NV: "Nevada",
|
||||
NH: "New Hampshire",
|
||||
NJ: "New Jersey",
|
||||
NM: "New Mexico",
|
||||
NY: "New York",
|
||||
NC: "North Carolina",
|
||||
ND: "North Dakota",
|
||||
OH: "Ohio",
|
||||
OK: "Oklahoma",
|
||||
OR: "Oregon",
|
||||
PA: "Pennsylvania",
|
||||
PR: "Puerto Rico",
|
||||
RI: "Rhode Island",
|
||||
SC: "South Carolina",
|
||||
SD: "South Dakota",
|
||||
TN: "Tennessee",
|
||||
TX: "Texas",
|
||||
UT: "Utah",
|
||||
VT: "Vermont",
|
||||
VA: "Virginia",
|
||||
WA: "Washington",
|
||||
WV: "West Virginia",
|
||||
WI: "Wisconsin",
|
||||
WY: "Wyoming",
|
||||
AB: "Alberta",
|
||||
BC: "British Columbia",
|
||||
MB: "Manitoba",
|
||||
NB: "New Brunswick",
|
||||
NS: "Nova Scotia",
|
||||
ON: "Ontario",
|
||||
PE: "Prince Edward Island",
|
||||
QC: "Quebec",
|
||||
SK: "Saskatchewan",
|
||||
YT: "Yukon",
|
||||
CN: "Canada",
|
||||
AE: "Military Address - AE",
|
||||
AP: "Military Address - AP",
|
||||
AA: "Military Address - AA",
|
||||
AS: "American Samoa - AS",
|
||||
MH: "Marshall Islands - MH",
|
||||
MP: "Mariana Islands - MP",
|
||||
NL: "Newfoundland and Labrador",
|
||||
PW: "Palau",
|
||||
VI: "Virgin Islands",
|
||||
};
|
||||
export { stateOptionsInternational };
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export default {
|
|||
// Format used for the v-model value emitted to parent
|
||||
modelType: {
|
||||
type: String,
|
||||
default: "MM/dd/yyyy",
|
||||
default: "yyyy-MM-dd",
|
||||
},
|
||||
enableTimePicker: {
|
||||
type: Boolean,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { getSessionKeyValue } from "@/helpers/heritage-integration/cookie-helper
|
|||
import { headerKeys } from "@/constants/header-keys";
|
||||
import axiosResponseInterceptorMessages from "@/constants/axios-response-interceptor-messages.js";
|
||||
import { endpoints } from "@/constants/endpoints";
|
||||
import { getDeviceIdValue } from "@/helpers/heritage-integration/cookie-helper";
|
||||
|
||||
// Add a response interceptor for global axios error handing.
|
||||
axios.interceptors.response.use(
|
||||
|
|
@ -73,6 +74,7 @@ export default {
|
|||
[headerKeys.TRANSACTION_ID]: crypto.randomUUID(),
|
||||
[headerKeys.EON]: order?.eon,
|
||||
[headerKeys.LOG_ENABLED]: store.getters.applicationUser?.loggingOption ?? false,
|
||||
[headerKeys.DEVICE_ID]: getDeviceIdValue(),
|
||||
};
|
||||
|
||||
axios({
|
||||
|
|
|
|||
|
|
@ -35,7 +35,9 @@ export function fetchCmsContentForPage(fmgPage) {
|
|||
// Client-specific config pages are optional. When one doesn't exist
|
||||
// the CMS returns a 404 - treat that as "no overrides" instead of an
|
||||
// error so callers receive an empty config object.
|
||||
const isClientConfigPage = typeof fmgPage === "string" && fmgPage.startsWith("client_");
|
||||
const isClientConfigPage =
|
||||
typeof fmgPage === "string" &&
|
||||
(fmgPage.startsWith("client_") || fmgPage.startsWith("unmanaged_"));
|
||||
const status = error?.status ?? error?.response?.status;
|
||||
const isNotFound = status == 404;
|
||||
|
||||
|
|
|
|||
|
|
@ -8,13 +8,14 @@
|
|||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||
<div class="my-5" ref="unverified-block">
|
||||
<div v-if="showUnverifiedContent" class="my-5" ref="unverified-block">
|
||||
<funnelSubHeader cmsWidgetName="SubHeaderWidgetUnverified" alignLeft />
|
||||
<customerInstructions
|
||||
cmsWidgetName="InstructionsWidgetUnverified"
|
||||
:cmsWidgetName="getUnverifiedInstructionsWidgetName"
|
||||
:isRepair="isRepair"
|
||||
v-on="{ textLinkClicked: openModalAction }" />
|
||||
</div>
|
||||
<div class="my-5" ref="verified-block">
|
||||
<div v-if="showVerifiedContent" class="my-5" ref="verified-block">
|
||||
<funnelSubHeader cmsWidgetName="SubHeaderWidgetVerified" alignLeft />
|
||||
<priceDisplay
|
||||
cmsWidgetName="PriceWidgetVerified"
|
||||
|
|
@ -27,7 +28,7 @@
|
|||
cmsWidgetName="AfterpayBreakoutWidget"
|
||||
:totalAmount="cashPrice" />
|
||||
</div>
|
||||
<div class="my-5" ref="itac-block">
|
||||
<div v-if="showItacContent" class="my-5" ref="itac-block">
|
||||
<funnelSubHeader
|
||||
class="mb-4"
|
||||
cmsWidgetName="SubHeaderWidgetItac"
|
||||
|
|
@ -39,7 +40,7 @@
|
|||
<priceDisplay cmsWidgetName="PriceWidgetItac" :price="cashPrice" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-5" ref="nocomp-block">
|
||||
<div v-if="showNoCompContent" class="my-5" ref="nocomp-block">
|
||||
<funnelSubHeader
|
||||
class="mb-4"
|
||||
cmsWidgetName="SubHeaderWidgetNocomp"
|
||||
|
|
@ -92,7 +93,11 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
isClaimAndCoverage: this.isClaimAndCoverageFromStore(),
|
||||
isVerified: this.isVerifiedInsuranceFromStore(),
|
||||
isItac: this.isItacFromStore(),
|
||||
isNoComp: this.isNoCompFromStore(),
|
||||
isRecalibrationOnOrder: this.isRecalibrationOnOrderFromStore(),
|
||||
isRepair: this.isRepairFromStore(),
|
||||
clientConfig: {},
|
||||
};
|
||||
},
|
||||
|
|
@ -113,17 +118,21 @@ export default {
|
|||
const pageName = to.name;
|
||||
const cmsContentPromise = fetchCmsContentForPage(pageName);
|
||||
const clientConfigPageName = `client_${store.getters.order.payment.parentAccountNumber}`;
|
||||
const clientConfigPromise = fetchCmsContentForPage(clientConfigPageName);
|
||||
const promiseResultMap = [
|
||||
{
|
||||
resultKey: "cmsContent",
|
||||
promise: cmsContentPromise,
|
||||
},
|
||||
{
|
||||
resultKey: "clientConfig",
|
||||
promise: clientConfigPromise,
|
||||
},
|
||||
];
|
||||
|
||||
// Only fetch the client-specific config when this is a claim & coverage order.
|
||||
if (store.getters.order?.payment?.isClaimAndCoverage) {
|
||||
promiseResultMap.push({
|
||||
resultKey: "clientConfig",
|
||||
promise: fetchCmsContentForPage(clientConfigPageName),
|
||||
});
|
||||
}
|
||||
|
||||
const resultMap = await settleAllPromises(promiseResultMap);
|
||||
next((vm) => {
|
||||
vm.setCmsContent(resultMap.cmsContent);
|
||||
|
|
@ -139,9 +148,21 @@ export default {
|
|||
isClaimAndCoverageFromStore() {
|
||||
return store.getters.order?.payment?.isClaimAndCoverage;
|
||||
},
|
||||
isVerifiedInsuranceFromStore() {
|
||||
return store.getters.order?.payment?.insuranceCoverage.isVerified;
|
||||
},
|
||||
isItacFromStore() {
|
||||
return store.getters.order?.policy?.isItac;
|
||||
},
|
||||
isNoCompFromStore() {
|
||||
return store.getters.order?.policy?.isNoComp;
|
||||
},
|
||||
isRecalibrationOnOrderFromStore() {
|
||||
return store.getters.isRecalibrationOnOrder;
|
||||
},
|
||||
isRepairFromStore() {
|
||||
return store.getters.order?.damage?.isRepair;
|
||||
},
|
||||
backButtonAction() {
|
||||
if (!this.isClaimAndCoverage) {
|
||||
this.$router.navigateWithoutSaving(
|
||||
|
|
@ -178,6 +199,27 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
showVerifiedContent() {
|
||||
return this.isVerified && !this.isItac && !this.isNoComp;
|
||||
},
|
||||
showUnverifiedContent() {
|
||||
return !this.isVerified;
|
||||
},
|
||||
showItacContent() {
|
||||
return this.isItac;
|
||||
},
|
||||
showNoCompContent() {
|
||||
return this.isNoComp;
|
||||
},
|
||||
getUnverifiedInstructionsWidgetName() {
|
||||
if (!this.isVerified) {
|
||||
if (this.isRepair) {
|
||||
return "InstructionsWidgetUnverifiedRepair";
|
||||
}
|
||||
return "InstructionsWidgetUnverified";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
clientLogoImageSrc() {
|
||||
return this.clientConfig?.ClientLogoWidget?.Image || "";
|
||||
},
|
||||
|
|
@ -201,6 +243,9 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.funnel-sub-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.price-compare {
|
||||
display: flex;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
{{ headerText }}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="!this.isRepair">
|
||||
<ol>
|
||||
<li v-for="block in instructionBlocks" :key="block">
|
||||
<li v-for="block in InstructionBlocks" :key="block">
|
||||
<span v-for="token in block" :key="token">
|
||||
<span v-if="doesCopyContainTextLink(token)" class="link">
|
||||
<textLink
|
||||
|
|
@ -24,6 +24,7 @@
|
|||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div v-else v-html="RepairInstructionText"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -45,6 +46,7 @@ export default {
|
|||
},
|
||||
props: {
|
||||
cmsWidgetName: String,
|
||||
isRepair: Boolean,
|
||||
},
|
||||
methods: {
|
||||
doesCopyContainTextLink,
|
||||
|
|
@ -57,7 +59,7 @@ export default {
|
|||
headerText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
|
||||
},
|
||||
instructionBlocks() {
|
||||
InstructionBlocks() {
|
||||
const answers = this.getCmsContent(this.cmsWidgetName, "Answers");
|
||||
|
||||
if (!answers) {
|
||||
|
|
@ -72,6 +74,9 @@ export default {
|
|||
|
||||
return instructionBlocksAsTokens;
|
||||
},
|
||||
RepairInstructionText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "Text");
|
||||
},
|
||||
},
|
||||
components: {
|
||||
textLink,
|
||||
|
|
|
|||
|
|
@ -277,7 +277,16 @@ export default {
|
|||
var nonIntegratedTestParentAccount = this.getSettingValue(
|
||||
experimentSettings.NON_CLAIM_AND_COVERAGE_TEST_PARENT_ACCOUNT
|
||||
);
|
||||
if (nonIntegratedTestParentAccount === this.selectedAccount.parentAccountNumber) {
|
||||
const nonIntegratedTestParentAccounts = (nonIntegratedTestParentAccount ?? "")
|
||||
.toString()
|
||||
.split(",")
|
||||
.map((account) => account.trim())
|
||||
.filter((account) => account.length > 0);
|
||||
if (
|
||||
nonIntegratedTestParentAccounts.includes(
|
||||
this.selectedAccount.parentAccountNumber?.toString()
|
||||
)
|
||||
) {
|
||||
debugLog(
|
||||
`--- ${this.selectedAccount.parentAccountNumber} Non integrated experiment setting is false, but a parent account is NON_CLAIM_AND_COVERAGE_TEST_PARENT_ACCOUNT experiment contains a specific parent account number, sending to vue app Insurance Details page ---`
|
||||
);
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
placeholderText="MM/DD/YYYY"
|
||||
validationRules="date-of-loss-required"
|
||||
format="MM/dd/yyyy"
|
||||
modelType="MM/dd/yyyy"
|
||||
modelType="yyyy-MM-dd"
|
||||
:enableTimePicker="false"
|
||||
:clearable="false"
|
||||
:textInput="true"
|
||||
|
|
@ -146,14 +146,11 @@ import { errorMessages } from "@/constants/error-messages";
|
|||
import { required } from "@/helpers/validation-rules";
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
import { stateOptions } from "@/constants/state-options";
|
||||
import { stateOptionsInternational } from "@/constants/state-options";
|
||||
import { regex } from "@/helpers/validation-rules";
|
||||
import {
|
||||
NON_MANAGED_SHOW_CLAIM_NUMBER_PARENTS,
|
||||
coverageStatus,
|
||||
coverageStatusEnum,
|
||||
} from "@/constants/insurance";
|
||||
import { coverageStatus, coverageStatusEnum } from "@/constants/insurance";
|
||||
import saveProgressModalQuestion from "@/fmg-components/save-progress-modal-question/save-progress-modal-question";
|
||||
import { debugLog } from "@/helpers/debug-log-helper";
|
||||
|
||||
// DEFINE VALIDATION RULES
|
||||
defineRule(
|
||||
|
|
@ -174,6 +171,7 @@ export default {
|
|||
mixins: [],
|
||||
data() {
|
||||
return {
|
||||
clientConfig: {},
|
||||
insuranceCompanyName: this.getInsuranceCompanyNameFromStore(),
|
||||
policyNumber: this.getPolicyNumberFromStore(),
|
||||
dateOfLoss: this.getDateOfLossFromStore(),
|
||||
|
|
@ -202,11 +200,17 @@ export default {
|
|||
async beforeRouteEnter(to, from, next) {
|
||||
const pageName = to.name;
|
||||
const cmsContentPromise = fetchCmsContentForPage(pageName);
|
||||
const clientConfigPageName = `unmanaged_${store.getters.order.payment.parentAccountNumber}`;
|
||||
const clientConfigPromise = fetchCmsContentForPage(clientConfigPageName);
|
||||
const promiseResultMap = [
|
||||
{
|
||||
resultKey: "cmsContent",
|
||||
promise: cmsContentPromise,
|
||||
},
|
||||
{
|
||||
resultKey: "clientConfig",
|
||||
promise: clientConfigPromise,
|
||||
},
|
||||
];
|
||||
const resultMap = await settleAllPromises(promiseResultMap);
|
||||
const emailFromStore = store.getters.order.customer.emailAddress;
|
||||
|
|
@ -214,6 +218,14 @@ export default {
|
|||
next((vm) => {
|
||||
vm.setCmsContent(resultMap.cmsContent);
|
||||
vm.showSaveProgressModal = showSaveProgressModal;
|
||||
vm.clientConfig = resultMap.clientConfig ?? {};
|
||||
// The load-session API can return damageCause in a different case than
|
||||
// the CMS answer values. Reconcile it so the dropdown pre-selects.
|
||||
vm.damageCause = vm.matchDamageCauseToOption(vm.damageCause);
|
||||
debugLog(
|
||||
`insurance-details clientConfig::${clientConfigPageName}`,
|
||||
JSON.stringify(vm.clientConfig)
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
@ -221,16 +233,21 @@ export default {
|
|||
today() {
|
||||
return new Date();
|
||||
},
|
||||
parsedClientConfig() {
|
||||
const raw = this.clientConfig?.ConfigWidget?.Text;
|
||||
if (!raw) return {};
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (err) {
|
||||
console.warn("Failed to parse clientConfig.ConfigWidget.Text", err);
|
||||
return {};
|
||||
}
|
||||
},
|
||||
displayClaimNumber() {
|
||||
const order = store.getters.order;
|
||||
|
||||
const parent = NON_MANAGED_SHOW_CLAIM_NUMBER_PARENTS.find(
|
||||
(item) => item.value === order.payment?.parentAccountNumber?.toString()
|
||||
);
|
||||
return !!parent;
|
||||
return this.parsedClientConfig?.displayClaimNumberField ?? false;
|
||||
},
|
||||
stateOptions() {
|
||||
return stateOptions;
|
||||
return stateOptionsInternational;
|
||||
},
|
||||
damageCauseQuestionText() {
|
||||
return this.getCmsContent("DamageTypesWidget", "QuestionText");
|
||||
|
|
@ -239,6 +256,9 @@ export default {
|
|||
const options = {};
|
||||
|
||||
const answers = this.getCmsContent("DamageTypesWidget", "Answers");
|
||||
if (!Array.isArray(answers)) {
|
||||
return options;
|
||||
}
|
||||
for (const answer of answers) {
|
||||
options[answer.Name] = answer.Text;
|
||||
}
|
||||
|
|
@ -259,23 +279,36 @@ export default {
|
|||
getDamageCauseFromStore() {
|
||||
return store.getters.order.damage.damageCause ?? "";
|
||||
},
|
||||
// Maps a stored damageCause to the matching dropdown option value using a
|
||||
// case-insensitive comparison (load-session can return it uppercased).
|
||||
// Returns the canonical option value, or the original value if no match.
|
||||
matchDamageCauseToOption(value) {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
const optionValues = Object.values(this.damageTypesOptions ?? {});
|
||||
const match = optionValues.find(
|
||||
(option) => option?.toLowerCase() === value.toLowerCase()
|
||||
);
|
||||
return match ?? value;
|
||||
},
|
||||
getStreetAddressFromStore() {
|
||||
return store.getters.order.policy.policyAddress.streetAddress ?? "";
|
||||
return store.getters.order.customer.address.streetAddress ?? "";
|
||||
},
|
||||
getStreetAddress2FromStore() {
|
||||
return store.getters.order.policy.policyAddress.streetAddress2 ?? "";
|
||||
return store.getters.order.customer.address.streetAddress2 ?? "";
|
||||
},
|
||||
getCityFromStore() {
|
||||
return store.getters.order.policy.policyAddress.city ?? "";
|
||||
return store.getters.order.customer.address.city ?? "";
|
||||
},
|
||||
getPolicyStateFromStore() {
|
||||
return store.getters.order.policy.policyAddress.state ?? "";
|
||||
return store.getters.order.customer.address.state ?? "";
|
||||
},
|
||||
getPolicyZipFromStore() {
|
||||
return store.getters.order.policy.policyAddress.zipCode ?? "";
|
||||
return store.getters.order.customer.address.zipCode ?? "";
|
||||
},
|
||||
getClaimNumberFromStore() {
|
||||
return store.getters.order.policy.claimNumber ?? "";
|
||||
return store.getters.order.payment.insuranceCoverage.claimNumber ?? "";
|
||||
},
|
||||
backButtonAction() {
|
||||
this.$router.navigateWithoutSaving(
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
placeholderText="MM/DD/YYYY"
|
||||
validationRules="date-of-birth-required"
|
||||
format="MM/dd/yyyy"
|
||||
modelType="MM/dd/yyyy"
|
||||
modelType="yyyy-MM-dd"
|
||||
:enableTimePicker="false"
|
||||
:textInput="true"
|
||||
:maxDate="today"
|
||||
|
|
@ -100,7 +100,7 @@
|
|||
placeholderText="MM/DD/YYYY"
|
||||
validationRules="date-of-loss-required"
|
||||
format="MM/dd/yyyy"
|
||||
modelType="MM/dd/yyyy"
|
||||
modelType="yyyy-MM-dd"
|
||||
:enableTimePicker="false"
|
||||
:textInput="true"
|
||||
:maxDate="today"
|
||||
|
|
@ -314,6 +314,9 @@ export default {
|
|||
const options = {};
|
||||
|
||||
const answers = this.getCmsContent("DamageTypesWidget", "Answers");
|
||||
if (!Array.isArray(answers)) {
|
||||
return options;
|
||||
}
|
||||
for (const answer of answers) {
|
||||
options[answer.Name] = answer.Text;
|
||||
}
|
||||
|
|
@ -411,6 +414,9 @@ export default {
|
|||
next((vm) => {
|
||||
vm.setCmsContent(resultMap.cmsContent);
|
||||
vm.clientConfig = resultMap.clientConfig ?? {};
|
||||
// The load-session API can return damageCause in a different case than
|
||||
// the CMS answer values. Reconcile it so the dropdown pre-selects.
|
||||
vm.damageCause = vm.matchDamageCauseToOption(vm.damageCause);
|
||||
debugLog(
|
||||
`policy-infoclientConfig::${clientConfigPageName}`,
|
||||
JSON.stringify(vm.clientConfig)
|
||||
|
|
@ -437,6 +443,19 @@ export default {
|
|||
getDamageCauseFromStore() {
|
||||
return store.getters.order.damage.damageCause;
|
||||
},
|
||||
// Maps a stored damageCause to the matching dropdown option value using a
|
||||
// case-insensitive comparison (load-session can return it uppercased).
|
||||
// Returns the canonical option value, or the original value if no match.
|
||||
matchDamageCauseToOption(value) {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
const optionValues = Object.values(this.damageTypesOptions ?? {});
|
||||
const match = optionValues.find(
|
||||
(option) => option?.toLowerCase() === value.toLowerCase()
|
||||
);
|
||||
return match ?? value;
|
||||
},
|
||||
getPolicyStateFromStore() {
|
||||
return store.getters.order.policy.state;
|
||||
},
|
||||
|
|
|
|||
44
src/layouts/schedule/helpers/schedule-helper.inshop.spec.js
Normal file
44
src/layouts/schedule/helpers/schedule-helper.inshop.spec.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { RouteCodeFlags } from "@/constants/schedule-constants";
|
||||
import {
|
||||
getInshopAppointmentTimeSlots,
|
||||
groupInshopTimeSlotsByTimeOfDay,
|
||||
mapInshopTimeSlotToDisplaySlot,
|
||||
} from "./schedule-helper";
|
||||
|
||||
describe("schedule-helper inshop time slots", () => {
|
||||
const dropOffSlot = {
|
||||
id: `route-${RouteCodeFlags.ALL_DAY_DROP_OFF}`,
|
||||
startTime: "08:00",
|
||||
endTime: "17:00",
|
||||
};
|
||||
const morningSlot = { id: "route-morning", startTime: "09:30", endTime: "10:00" };
|
||||
const afternoonSlot = { id: "route-afternoon", startTime: "13:00", endTime: "13:30" };
|
||||
|
||||
test("getInshopAppointmentTimeSlots excludes drop off route codes", () => {
|
||||
const result = getInshopAppointmentTimeSlots([dropOffSlot, morningSlot, afternoonSlot]);
|
||||
expect(result).toEqual([morningSlot, afternoonSlot]);
|
||||
});
|
||||
|
||||
test("groupInshopTimeSlotsByTimeOfDay splits and sorts slots by start time", () => {
|
||||
const lateMorning = { id: "route-late-morning", startTime: "11:30", endTime: "12:00" };
|
||||
const earlyMorning = { id: "route-early-morning", startTime: "08:00", endTime: "08:30" };
|
||||
|
||||
const result = groupInshopTimeSlotsByTimeOfDay([
|
||||
afternoonSlot,
|
||||
dropOffSlot,
|
||||
lateMorning,
|
||||
earlyMorning,
|
||||
]);
|
||||
|
||||
expect(result.morning).toEqual([earlyMorning, lateMorning]);
|
||||
expect(result.afternoon).toEqual([afternoonSlot]);
|
||||
});
|
||||
|
||||
test("mapInshopTimeSlotToDisplaySlot formats label and route code", () => {
|
||||
expect(mapInshopTimeSlotToDisplaySlot(morningSlot)).toEqual({
|
||||
value: "route-morning",
|
||||
label: "9:30 AM",
|
||||
routeCodeId: "route-morning",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -72,6 +72,50 @@ export function isDropOffRouteCode(routeCode) {
|
|||
);
|
||||
}
|
||||
|
||||
export const INSHOP_INITIAL_VISIBLE_TIME_SLOTS = 6;
|
||||
|
||||
export function getInshopAppointmentTimeSlots(timeSlots = []) {
|
||||
return (timeSlots ?? []).filter((timeSlot) => !isDropOffRouteCode(timeSlot.id));
|
||||
}
|
||||
|
||||
export function isMorningInshopTimeSlot(timeSlot) {
|
||||
const hours = parseInt(timeSlot?.startTime?.split(":")[0], 10);
|
||||
if (Number.isNaN(hours)) {
|
||||
return false;
|
||||
}
|
||||
return hours < 12;
|
||||
}
|
||||
|
||||
function sortTimeSlotsByStartTime(timeSlots) {
|
||||
return [...timeSlots].sort((a, b) => {
|
||||
if (a.startTime < b.startTime) return -1;
|
||||
if (a.startTime > b.startTime) return 1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
export function groupInshopTimeSlotsByTimeOfDay(timeSlots = []) {
|
||||
const inshopSlots = getInshopAppointmentTimeSlots(timeSlots);
|
||||
const morning = sortTimeSlotsByStartTime(inshopSlots.filter(isMorningInshopTimeSlot));
|
||||
const afternoon = sortTimeSlotsByStartTime(
|
||||
inshopSlots.filter((timeSlot) => !isMorningInshopTimeSlot(timeSlot))
|
||||
);
|
||||
|
||||
return { morning, afternoon };
|
||||
}
|
||||
|
||||
export function formatInshopTimeSlotLabel(timeSlot) {
|
||||
return militaryToTwelveHourTime(timeSlot.startTime);
|
||||
}
|
||||
|
||||
export function mapInshopTimeSlotToDisplaySlot(timeSlot) {
|
||||
return {
|
||||
value: timeSlot.id,
|
||||
label: formatInshopTimeSlotLabel(timeSlot),
|
||||
routeCodeId: timeSlot.id,
|
||||
};
|
||||
}
|
||||
|
||||
export function getTodayDate() {
|
||||
return new Date();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,246 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import inshopSchedulingCard from "./inshop-scheduling-card";
|
||||
import { RouteCodeFlags, AppointmentTypeStrings } from "@/constants/schedule-constants";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
|
||||
const ALL_DAY_DROPOFF_ROUTE_CODE = `03357-001820-I*21346*${RouteCodeFlags.ALL_DAY_DROP_OFF}`;
|
||||
const OVERNIGHT_DROPOFF_ROUTE_CODE = `03357-001820-I*21346*${RouteCodeFlags.OVERNIGHT_DROP_OFF}`;
|
||||
const MORNING_INSHOP_ROUTE_CODE = "03357-001820-I*21346*0800";
|
||||
const AFTERNOON_INSHOP_ROUTE_CODE = "03357-001820-I*21346*1300";
|
||||
|
||||
const MOCK_TIME_SLOTS = [
|
||||
{ id: ALL_DAY_DROPOFF_ROUTE_CODE, startTime: "08:00", endTime: "17:00" },
|
||||
{ id: OVERNIGHT_DROPOFF_ROUTE_CODE, startTime: "17:00", endTime: "08:00" },
|
||||
{ id: MORNING_INSHOP_ROUTE_CODE, startTime: "08:00", endTime: "08:30" },
|
||||
{ id: "03357-001820-I*21346*0830", startTime: "08:30", endTime: "09:00" },
|
||||
{ id: "03357-001820-I*21346*0900", startTime: "09:00", endTime: "09:30" },
|
||||
{ id: AFTERNOON_INSHOP_ROUTE_CODE, startTime: "13:00", endTime: "13:30" },
|
||||
{ id: "03357-001820-I*21346*1400", startTime: "14:00", endTime: "14:30" },
|
||||
];
|
||||
|
||||
const MANY_MORNING_TIME_SLOTS = [
|
||||
{ id: ALL_DAY_DROPOFF_ROUTE_CODE, startTime: "08:00", endTime: "17:00" },
|
||||
...["08:00", "08:30", "09:00", "09:30", "10:00", "10:30", "11:00"].map((startTime, i) => ({
|
||||
id: `03357-001820-I*21346*M${i}`,
|
||||
startTime,
|
||||
endTime: startTime,
|
||||
})),
|
||||
];
|
||||
|
||||
const MOCK_PROVIDER = {
|
||||
providerNumber: "001820",
|
||||
distanceInMiles: 4.5,
|
||||
address: {
|
||||
city: "WORTHINGTON",
|
||||
state: "OH",
|
||||
streetAddress: "760 DEARBORN PARK LN",
|
||||
zipCode: "43085",
|
||||
},
|
||||
};
|
||||
|
||||
const MOCK_CMS_CONTENT = {
|
||||
InshopCardWidget: {
|
||||
BodyText: "Morning:",
|
||||
BodyText2: "Afternoon:",
|
||||
FooterText: "View more times",
|
||||
},
|
||||
DropoffQuestionWidget: {
|
||||
QuestionText: "Drop off your vehicle",
|
||||
Answers: [
|
||||
{
|
||||
Name: "AllDayDropOff",
|
||||
Text: "Drop off before 9:00 am",
|
||||
SubText:
|
||||
"Drop off your vehicle before 9:00 AM and pick it up between 4–5 PM. Park in any open spot and visit the shop's front desk to drop off your key.",
|
||||
},
|
||||
{
|
||||
Name: "OvernightDropOff",
|
||||
Text: "Overnight drop off",
|
||||
SubText:
|
||||
"Drop off your vehicle before closing and pick it up the next business day.",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
function mountComponent(props = {}, cmsContent = {}) {
|
||||
const cmsContentByWidget = {
|
||||
...MOCK_CMS_CONTENT,
|
||||
...cmsContent,
|
||||
InshopCardWidget: {
|
||||
...MOCK_CMS_CONTENT.InshopCardWidget,
|
||||
...cmsContent.InshopCardWidget,
|
||||
},
|
||||
DropoffQuestionWidget: {
|
||||
...MOCK_CMS_CONTENT.DropoffQuestionWidget,
|
||||
...cmsContent.DropoffQuestionWidget,
|
||||
},
|
||||
};
|
||||
|
||||
const cmsMixin = {
|
||||
methods: {
|
||||
getCmsContent: jest.fn((widgetName, fieldName) => {
|
||||
return cmsContentByWidget[widgetName]?.[fieldName] ?? "";
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const mountOptions = getMountOptions({
|
||||
route: { name: "scheduling" },
|
||||
mixins: [cmsMixin],
|
||||
});
|
||||
|
||||
return shallowMount(inshopSchedulingCard, {
|
||||
props: {
|
||||
provider: MOCK_PROVIDER,
|
||||
timeSlots: MOCK_TIME_SLOTS,
|
||||
radioGroupName: "inshopSchedulingTimeSlot-001820",
|
||||
...props,
|
||||
},
|
||||
global: mountOptions.global,
|
||||
});
|
||||
}
|
||||
|
||||
describe("inshop-scheduling-card.vue", () => {
|
||||
test("renders city, distance, address, and grouped appointment sections", () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain("Worthington");
|
||||
expect(wrapper.text()).toContain("4.5 mi");
|
||||
expect(wrapper.text()).toContain("760 Dearborn Park Ln, Worthington, OH 43085");
|
||||
expect(wrapper.text()).toContain("Drop off your vehicle");
|
||||
expect(wrapper.text()).toContain("Morning:");
|
||||
expect(wrapper.text()).toContain("Afternoon:");
|
||||
expect(wrapper.text()).toContain("8:00 AM");
|
||||
expect(wrapper.text()).toContain("1:00 PM");
|
||||
expect(wrapper.text()).toContain("7 appts");
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test("renders only drop off options that match available time slots", () => {
|
||||
const wrapper = mountComponent({
|
||||
timeSlots: [{ id: ALL_DAY_DROPOFF_ROUTE_CODE, startTime: "08:00", endTime: "17:00" }],
|
||||
});
|
||||
expect(wrapper.text()).toContain("Drop off before 9:00 am");
|
||||
expect(wrapper.text()).not.toContain("Overnight drop off");
|
||||
expect(wrapper.text()).not.toContain("Morning:");
|
||||
expect(wrapper.text()).toContain("1 appt");
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test("hides drop off section when no matching time slots are available", () => {
|
||||
const wrapper = mountComponent({ timeSlots: [] });
|
||||
expect(wrapper.text()).not.toContain("Drop off your vehicle");
|
||||
expect(wrapper.text()).toContain("0 appts");
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test("shows drop off SubText only when the option is selected", async () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).not.toContain("Park in any open spot");
|
||||
|
||||
const dropOffInput = wrapper.find(`input[value="${ALL_DAY_DROPOFF_ROUTE_CODE}"]`);
|
||||
await dropOffInput.setValue(true);
|
||||
await wrapper.setProps({
|
||||
modelValue: {
|
||||
appointmentType: AppointmentTypeStrings.IN_SHOP,
|
||||
providerNumber: "001820",
|
||||
routeCodeId: ALL_DAY_DROPOFF_ROUTE_CODE,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain("Park in any open spot");
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test("emits update:modelValue with route code when drop off is selected", async () => {
|
||||
const wrapper = mountComponent();
|
||||
const dropOffInput = wrapper.find(`input[value="${ALL_DAY_DROPOFF_ROUTE_CODE}"]`);
|
||||
await dropOffInput.setValue(true);
|
||||
expect(wrapper.emitted("update:modelValue")[0][0]).toEqual({
|
||||
appointmentType: AppointmentTypeStrings.IN_SHOP,
|
||||
providerNumber: "001820",
|
||||
routeCodeId: ALL_DAY_DROPOFF_ROUTE_CODE,
|
||||
});
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test("shows view more times link only when a section has more than six slots", () => {
|
||||
const wrapper = mountComponent({ timeSlots: MANY_MORNING_TIME_SLOTS });
|
||||
const viewMoreLinks = wrapper.findAll(".inshop-scheduling-card__view-more-link");
|
||||
expect(viewMoreLinks.length).toBe(1);
|
||||
expect(viewMoreLinks[0].text()).toBe("View more times");
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test("expands section when view more times is clicked", async () => {
|
||||
const wrapper = mountComponent({ timeSlots: MANY_MORNING_TIME_SLOTS });
|
||||
expect(wrapper.findAll(".inshop-scheduling-card__slot-input").length).toBe(7);
|
||||
|
||||
await wrapper.find(".inshop-scheduling-card__view-more-link").trigger("click");
|
||||
expect(wrapper.findAll(".inshop-scheduling-card__slot-input").length).toBe(8);
|
||||
expect(wrapper.find(".inshop-scheduling-card__view-more-link").exists()).toBe(false);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test("does not show view more times when six or fewer inshop slots are available", () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.find(".inshop-scheduling-card__view-more-link").exists()).toBe(false);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test("emits address-clicked when address link is clicked", async () => {
|
||||
const wrapper = mountComponent();
|
||||
await wrapper.find(".inshop-scheduling-card__address-link").trigger("click");
|
||||
expect(wrapper.emitted("address-clicked")).toBeTruthy();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test("emits update:modelValue with route code when an inshop time slot is selected", async () => {
|
||||
const wrapper = mountComponent();
|
||||
const morningInput = wrapper.find(`input[value="${MORNING_INSHOP_ROUTE_CODE}"]`);
|
||||
await morningInput.setValue(true);
|
||||
expect(wrapper.emitted("update:modelValue")[0][0]).toEqual({
|
||||
appointmentType: AppointmentTypeStrings.IN_SHOP,
|
||||
providerNumber: "001820",
|
||||
routeCodeId: MORNING_INSHOP_ROUTE_CODE,
|
||||
});
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test("highlights selected time slot", async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: {
|
||||
appointmentType: AppointmentTypeStrings.IN_SHOP,
|
||||
providerNumber: "001820",
|
||||
routeCodeId: MORNING_INSHOP_ROUTE_CODE,
|
||||
},
|
||||
});
|
||||
const selectedSlot = wrapper.find(".inshop-scheduling-card__slot--selected");
|
||||
expect(selectedSlot.exists()).toBe(true);
|
||||
expect(selectedSlot.text()).toContain("8:00 AM");
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test("does not highlight selection from another provider", () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: {
|
||||
appointmentType: AppointmentTypeStrings.IN_SHOP,
|
||||
providerNumber: "999999",
|
||||
routeCodeId: MORNING_INSHOP_ROUTE_CODE,
|
||||
},
|
||||
});
|
||||
expect(wrapper.find(".inshop-scheduling-card__slot--selected").exists()).toBe(false);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test("collapses and expands body when header is clicked", async () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.find(".inshop-scheduling-card__body").isVisible()).toBe(false);
|
||||
await wrapper.find(".inshop-scheduling-card__header").trigger("click");
|
||||
expect(wrapper.find(".inshop-scheduling-card__body").isVisible()).toBe(true);
|
||||
expect(wrapper.find(".inshop-scheduling-card__address-link").isVisible()).toBe(true);
|
||||
await wrapper.find(".inshop-scheduling-card__header").trigger("click");
|
||||
expect(wrapper.find(".inshop-scheduling-card__body").isVisible()).toBe(false);
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,506 @@
|
|||
<template>
|
||||
<div class="inshop-scheduling-card">
|
||||
<div class="inshop-scheduling-card__panel">
|
||||
<button
|
||||
type="button"
|
||||
class="inshop-scheduling-card__header"
|
||||
:aria-expanded="isExpanded"
|
||||
@click="toggleExpanded">
|
||||
<img
|
||||
class="inshop-scheduling-card__shop-icon"
|
||||
src="@/assets/img/shop.svg"
|
||||
alt=""
|
||||
aria-hidden="true" />
|
||||
<span class="inshop-scheduling-card__header-title">{{ cityLabel }}</span>
|
||||
<span class="inshop-scheduling-card__distance">{{ formattedDistance }}</span>
|
||||
<span
|
||||
class="inshop-scheduling-card__appt-badge"
|
||||
:class="{
|
||||
'inshop-scheduling-card__appt-badge--empty': appointmentCount === 0,
|
||||
}">
|
||||
{{ appointmentCountLabel }}
|
||||
</span>
|
||||
<img
|
||||
class="inshop-scheduling-card__chevron"
|
||||
:class="{ 'inshop-scheduling-card__chevron--expanded': isExpanded }"
|
||||
src="@/assets/img/icons/chevron-no-background.svg"
|
||||
alt=""
|
||||
aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inshop-scheduling-card__address-link"
|
||||
@click.stop="onAddressClick">
|
||||
{{ formattedAddress }}
|
||||
</button>
|
||||
<div v-show="isExpanded" class="inshop-scheduling-card__body">
|
||||
<div v-if="dropOffSection" class="inshop-scheduling-card__section">
|
||||
<p class="inshop-scheduling-card__section-label">{{ dropOffSection.label }}</p>
|
||||
<fieldset class="inshop-scheduling-card__time-slots">
|
||||
<legend class="sr-only">{{ dropOffSection.label }}</legend>
|
||||
<label
|
||||
v-for="slot in dropOffSection.slots"
|
||||
:key="slot.value"
|
||||
class="inshop-scheduling-card__slot inshop-scheduling-card__slot--full-width"
|
||||
:class="{
|
||||
'inshop-scheduling-card__slot--selected': isSlotSelected(slot),
|
||||
}">
|
||||
<input
|
||||
type="radio"
|
||||
class="inshop-scheduling-card__slot-input"
|
||||
:name="radioGroupName"
|
||||
:value="slot.value"
|
||||
:checked="isSlotSelected(slot)"
|
||||
@change="onTimeSlotSelected(slot)" />
|
||||
<span class="inshop-scheduling-card__slot-label">
|
||||
<span class="inshop-scheduling-card__slot-label-primary">
|
||||
{{ slot.label }}
|
||||
</span>
|
||||
<span
|
||||
v-if="slot.subText && isSlotSelected(slot)"
|
||||
class="inshop-scheduling-card__slot-label-secondary"
|
||||
v-html="slot.subText" />
|
||||
</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div
|
||||
v-for="section in displayTimeSlotSections"
|
||||
:key="section.key"
|
||||
class="inshop-scheduling-card__section">
|
||||
<p class="inshop-scheduling-card__section-label">{{ section.label }}</p>
|
||||
<fieldset
|
||||
class="inshop-scheduling-card__time-slots inshop-scheduling-card__time-slots--grid">
|
||||
<legend class="sr-only">{{ section.label }}</legend>
|
||||
<label
|
||||
v-for="slot in section.visibleSlots"
|
||||
:key="slot.value"
|
||||
class="inshop-scheduling-card__slot"
|
||||
:class="{
|
||||
'inshop-scheduling-card__slot--selected': isSlotSelected(slot),
|
||||
}">
|
||||
<input
|
||||
type="radio"
|
||||
class="inshop-scheduling-card__slot-input"
|
||||
:name="radioGroupName"
|
||||
:value="slot.value"
|
||||
:checked="isSlotSelected(slot)"
|
||||
@change="onTimeSlotSelected(slot)" />
|
||||
<span class="inshop-scheduling-card__slot-label">
|
||||
<span class="inshop-scheduling-card__slot-label-primary">
|
||||
{{ slot.label }}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<button
|
||||
v-if="section.showViewMoreLink"
|
||||
type="button"
|
||||
class="inshop-scheduling-card__view-more-link"
|
||||
@click="onViewMoreTimesClick(section.key)">
|
||||
{{ viewMoreTimesText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { RouteCodeFlags, AppointmentTypeStrings } from "@/constants/schedule-constants";
|
||||
import {
|
||||
groupInshopTimeSlotsByTimeOfDay,
|
||||
INSHOP_INITIAL_VISIBLE_TIME_SLOTS,
|
||||
mapInshopTimeSlotToDisplaySlot,
|
||||
} from "@/layouts/schedule/helpers/schedule-helper";
|
||||
|
||||
function toTitleCase(str) {
|
||||
if (!str) return "";
|
||||
return str.replace(
|
||||
/\w\S*/g,
|
||||
(txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
const DROPOFF_CMS_NAME_TO_ROUTE_FLAG = {
|
||||
AllDayDropOff: RouteCodeFlags.ALL_DAY_DROP_OFF,
|
||||
OvernightDropOff: RouteCodeFlags.OVERNIGHT_DROP_OFF,
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "inshop-scheduling-card",
|
||||
emits: ["update:modelValue", "address-clicked"],
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
provider: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
timeSlots: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
radioGroupName: {
|
||||
type: String,
|
||||
default: "schedulingTimeSlot",
|
||||
},
|
||||
cmsWidgetName: {
|
||||
type: String,
|
||||
default: "InshopCardWidget",
|
||||
},
|
||||
dropoffCmsWidgetName: {
|
||||
type: String,
|
||||
default: "DropoffQuestionWidget",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isExpanded: false,
|
||||
expandedTimeSlotSections: {
|
||||
morning: false,
|
||||
afternoon: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
timeSlots() {
|
||||
this.expandedTimeSlotSections = {
|
||||
morning: false,
|
||||
afternoon: false,
|
||||
};
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
cityLabel() {
|
||||
return toTitleCase(this.provider?.address?.city);
|
||||
},
|
||||
formattedDistance() {
|
||||
const miles = this.provider?.distanceInMiles;
|
||||
if (miles == null) {
|
||||
return "";
|
||||
}
|
||||
const rounded = Math.round(miles * 10) / 10;
|
||||
return `${rounded} mi`;
|
||||
},
|
||||
formattedAddress() {
|
||||
const { streetAddress, city, state, zipCode } = this.provider?.address ?? {};
|
||||
if (!streetAddress) {
|
||||
return "";
|
||||
}
|
||||
return `${toTitleCase(streetAddress)}, ${toTitleCase(city)}, ${state} ${zipCode}`;
|
||||
},
|
||||
morningSectionLabel() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "BodyText");
|
||||
},
|
||||
afternoonSectionLabel() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "BodyText2");
|
||||
},
|
||||
viewMoreTimesText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "FooterText");
|
||||
},
|
||||
dropOffQuestionLabel() {
|
||||
return this.getCmsContent(this.dropoffCmsWidgetName, "QuestionText");
|
||||
},
|
||||
dropOffAnswers() {
|
||||
return this.getCmsContent(this.dropoffCmsWidgetName, "Answers") ?? [];
|
||||
},
|
||||
dropOffSection() {
|
||||
if (!this.dropOffQuestionLabel) {
|
||||
return null;
|
||||
}
|
||||
const slots = this.dropOffAnswers
|
||||
.map((answer) => this.buildDropOffSlotFromAnswer(answer))
|
||||
.filter(Boolean);
|
||||
if (!slots.length) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
label: this.dropOffQuestionLabel,
|
||||
slots,
|
||||
};
|
||||
},
|
||||
timeSlotSections() {
|
||||
const { morning, afternoon } = groupInshopTimeSlotsByTimeOfDay(this.timeSlots);
|
||||
|
||||
return [
|
||||
{
|
||||
key: "morning",
|
||||
label: this.morningSectionLabel,
|
||||
slots: morning.map(mapInshopTimeSlotToDisplaySlot),
|
||||
},
|
||||
{
|
||||
key: "afternoon",
|
||||
label: this.afternoonSectionLabel,
|
||||
slots: afternoon.map(mapInshopTimeSlotToDisplaySlot),
|
||||
},
|
||||
].filter((section) => section.label && section.slots.length);
|
||||
},
|
||||
displayTimeSlotSections() {
|
||||
return this.timeSlotSections.map((section) => {
|
||||
const isExpanded = this.expandedTimeSlotSections[section.key];
|
||||
const hasMoreThanInitialVisible =
|
||||
section.slots.length > INSHOP_INITIAL_VISIBLE_TIME_SLOTS;
|
||||
|
||||
return {
|
||||
...section,
|
||||
visibleSlots: isExpanded
|
||||
? section.slots
|
||||
: section.slots.slice(0, INSHOP_INITIAL_VISIBLE_TIME_SLOTS),
|
||||
showViewMoreLink: hasMoreThanInitialVisible && !isExpanded,
|
||||
};
|
||||
});
|
||||
},
|
||||
appointmentCount() {
|
||||
const dropOffCount = this.dropOffSection?.slots.length ?? 0;
|
||||
const timeSlotCount = this.timeSlotSections.reduce(
|
||||
(count, section) => count + section.slots.length,
|
||||
0
|
||||
);
|
||||
return dropOffCount + timeSlotCount;
|
||||
},
|
||||
appointmentCountLabel() {
|
||||
return `${this.appointmentCount} appt${this.appointmentCount === 1 ? "" : "s"}`;
|
||||
},
|
||||
selectedRouteCodeValue() {
|
||||
if (
|
||||
this.modelValue?.appointmentType !== AppointmentTypeStrings.IN_SHOP ||
|
||||
this.modelValue?.providerNumber !== this.provider.providerNumber
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return this.modelValue?.routeCodeId ?? null;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleExpanded() {
|
||||
this.isExpanded = !this.isExpanded;
|
||||
},
|
||||
onAddressClick() {
|
||||
this.$emit("address-clicked");
|
||||
},
|
||||
onViewMoreTimesClick(sectionKey) {
|
||||
this.expandedTimeSlotSections = {
|
||||
...this.expandedTimeSlotSections,
|
||||
[sectionKey]: true,
|
||||
};
|
||||
},
|
||||
findDropOffTimeSlotForCmsName(cmsName) {
|
||||
const routeFlag = DROPOFF_CMS_NAME_TO_ROUTE_FLAG[cmsName];
|
||||
if (!routeFlag) {
|
||||
return null;
|
||||
}
|
||||
return this.timeSlots.find((timeSlot) => timeSlot.id?.includes(routeFlag)) ?? null;
|
||||
},
|
||||
buildDropOffSlotFromAnswer(answer) {
|
||||
const timeSlot = this.findDropOffTimeSlotForCmsName(answer.Name);
|
||||
if (!timeSlot) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
value: timeSlot.id,
|
||||
label: answer.Text,
|
||||
subText: answer.SubText || null,
|
||||
routeCodeId: timeSlot.id,
|
||||
};
|
||||
},
|
||||
isSlotSelected(slot) {
|
||||
return this.selectedRouteCodeValue === slot.routeCodeId;
|
||||
},
|
||||
onTimeSlotSelected(slot) {
|
||||
this.$emit("update:modelValue", {
|
||||
appointmentType: AppointmentTypeStrings.IN_SHOP,
|
||||
providerNumber: this.provider.providerNumber,
|
||||
routeCodeId: slot.routeCodeId,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.inshop-scheduling-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
&__panel {
|
||||
width: 100%;
|
||||
border: 1px solid $gray-1000;
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
background: $white;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&__shop-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__header-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.5rem;
|
||||
color: $black;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__distance {
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5rem;
|
||||
color: $gray-600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__appt-badge {
|
||||
background: $blue-150;
|
||||
color: $blue;
|
||||
border-radius: 72px;
|
||||
padding: 2px 8px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.25rem;
|
||||
white-space: nowrap;
|
||||
|
||||
&--empty {
|
||||
background: $gray-100;
|
||||
color: $gray-500;
|
||||
}
|
||||
}
|
||||
|
||||
&__chevron {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
transform: rotate(180deg);
|
||||
transition: transform 150ms linear;
|
||||
|
||||
&--expanded {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__address-link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
margin-top: 4px;
|
||||
color: $blue;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.5rem;
|
||||
text-decoration: underline;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__body {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
&__section-label {
|
||||
margin: 0 0 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.5rem;
|
||||
color: $black;
|
||||
}
|
||||
|
||||
&__time-slots {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
&--grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__slot {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
border: 1px solid $gray-1000;
|
||||
border-radius: 8px;
|
||||
padding: 12px 8px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
cursor: pointer;
|
||||
background: $white;
|
||||
|
||||
&--selected {
|
||||
border: 2px solid $blue;
|
||||
background: $blue-150;
|
||||
}
|
||||
|
||||
&--full-width {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__slot-input {
|
||||
margin-top: 4px;
|
||||
accent-color: $blue;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__slot-label {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__slot-label-primary,
|
||||
&__slot-label-secondary {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
color: $gray-600;
|
||||
}
|
||||
|
||||
&__slot--selected &__slot-label-primary {
|
||||
color: $black;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__view-more-link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
color: $blue;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.5rem;
|
||||
text-decoration: underline;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import mobileSchedulingCard from "./mobile-scheduling-card";
|
||||
import { PREMIUM_TIME_SLOT_ID_FLAG } from "@/constants/schedule-constants";
|
||||
import { PREMIUM_TIME_SLOT_ID_FLAG, AppointmentTypeStrings } from "@/constants/schedule-constants";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
|
||||
const MOCK_TIME_SLOTS = [
|
||||
|
|
@ -54,6 +54,7 @@ function mountComponent(props = {}, cmsContent = {}) {
|
|||
return shallowMount(mobileSchedulingCard, {
|
||||
props: {
|
||||
zipCode: "43235",
|
||||
providerNumber: "03357",
|
||||
timeSlots: MOCK_TIME_SLOTS,
|
||||
premiumTimeSlotPrice: 29.99,
|
||||
...props,
|
||||
|
|
@ -102,10 +103,10 @@ describe("mobile-scheduling-card.vue", () => {
|
|||
);
|
||||
await premiumInput.setValue(true);
|
||||
expect(wrapper.emitted("update:modelValue")[0][0]).toEqual({
|
||||
selectedMobileTimeSlot: {
|
||||
routeCodeId: MOCK_TIME_SLOTS[0].id,
|
||||
isPremiumAppointment: true,
|
||||
},
|
||||
appointmentType: AppointmentTypeStrings.MOBILE,
|
||||
providerNumber: "03357",
|
||||
routeCodeId: MOCK_TIME_SLOTS[0].id,
|
||||
isPremiumAppointment: true,
|
||||
});
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
|
@ -120,10 +121,10 @@ describe("mobile-scheduling-card.vue", () => {
|
|||
await premiumInput.setValue(true);
|
||||
await wrapper.setProps({
|
||||
modelValue: {
|
||||
selectedMobileTimeSlot: {
|
||||
routeCodeId: MOCK_TIME_SLOTS[0].id,
|
||||
isPremiumAppointment: true,
|
||||
},
|
||||
appointmentType: AppointmentTypeStrings.MOBILE,
|
||||
providerNumber: "03357",
|
||||
routeCodeId: MOCK_TIME_SLOTS[0].id,
|
||||
isPremiumAppointment: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -136,11 +137,23 @@ describe("mobile-scheduling-card.vue", () => {
|
|||
const pmInput = wrapper.find(`input[value="${MOCK_TIME_SLOTS[1].id}"]`);
|
||||
await pmInput.setValue(true);
|
||||
expect(wrapper.emitted("update:modelValue")[0][0]).toEqual({
|
||||
selectedMobileTimeSlot: {
|
||||
routeCodeId: MOCK_TIME_SLOTS[1].id,
|
||||
isPremiumAppointment: false,
|
||||
appointmentType: AppointmentTypeStrings.MOBILE,
|
||||
providerNumber: "03357",
|
||||
routeCodeId: MOCK_TIME_SLOTS[1].id,
|
||||
isPremiumAppointment: false,
|
||||
});
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test("does not highlight selection from in-shop appointment", async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: {
|
||||
appointmentType: AppointmentTypeStrings.IN_SHOP,
|
||||
providerNumber: "001820",
|
||||
routeCodeId: MOCK_TIME_SLOTS[0].id,
|
||||
},
|
||||
});
|
||||
expect(wrapper.find(".mobile-scheduling-card__slot--selected").exists()).toBe(false);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
<img
|
||||
class="mobile-scheduling-card__chevron"
|
||||
:class="{ 'mobile-scheduling-card__chevron--expanded': isExpanded }"
|
||||
src="@/assets/img/icons/chevron-right-blue.svg"
|
||||
src="@/assets/img/icons/chevron-no-background.svg"
|
||||
alt=""
|
||||
aria-hidden="true" />
|
||||
</button>
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
<input
|
||||
type="radio"
|
||||
class="mobile-scheduling-card__slot-input"
|
||||
name="mobileSchedulingTimeSlot"
|
||||
:name="radioGroupName"
|
||||
:value="slot.value"
|
||||
:checked="isSlotSelected(slot)"
|
||||
@change="onTimeSlotSelected(slot)" />
|
||||
|
|
@ -84,7 +84,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { PREMIUM_TIME_SLOT_ID_FLAG } from "@/constants/schedule-constants";
|
||||
import { PREMIUM_TIME_SLOT_ID_FLAG, AppointmentTypeStrings } from "@/constants/schedule-constants";
|
||||
import { militaryToTwelveHourTime } from "@/layouts/schedule/helpers/schedule-helper";
|
||||
|
||||
export default {
|
||||
|
|
@ -107,6 +107,14 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
providerNumber: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
radioGroupName: {
|
||||
type: String,
|
||||
default: "schedulingTimeSlot",
|
||||
},
|
||||
showFreeFlag: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
|
@ -144,7 +152,7 @@ export default {
|
|||
return `${this.appointmentCount} appt${this.appointmentCount === 1 ? "" : "s"}`;
|
||||
},
|
||||
hasPremiumTimeSlot() {
|
||||
return this.timeSlots?.length > 0; //this.timeSlots?.[0]?.offerPremium === true && this.premiumTimeSlotPrice != null;
|
||||
return this.timeSlots?.[0]?.offerPremium === true && this.premiumTimeSlotPrice != null;
|
||||
},
|
||||
displayTimeSlots() {
|
||||
if (!this.timeSlots?.length) {
|
||||
|
|
@ -179,13 +187,18 @@ export default {
|
|||
return `+$${this.premiumTimeSlotPrice.toFixed(2)}`;
|
||||
},
|
||||
selectedRouteCodeValue() {
|
||||
const selected = this.modelValue?.selectedMobileTimeSlot;
|
||||
if (!selected?.routeCodeId) {
|
||||
if (
|
||||
this.modelValue?.appointmentType !== AppointmentTypeStrings.MOBILE ||
|
||||
this.modelValue?.providerNumber !== this.providerNumber
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return selected.isPremiumAppointment
|
||||
? this.addPremiumFlagToInput(selected.routeCodeId)
|
||||
: selected.routeCodeId;
|
||||
if (!this.modelValue?.routeCodeId) {
|
||||
return null;
|
||||
}
|
||||
return this.modelValue.isPremiumAppointment
|
||||
? this.addPremiumFlagToInput(this.modelValue.routeCodeId)
|
||||
: this.modelValue.routeCodeId;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -205,10 +218,10 @@ export default {
|
|||
},
|
||||
onTimeSlotSelected(slot) {
|
||||
this.$emit("update:modelValue", {
|
||||
selectedMobileTimeSlot: {
|
||||
routeCodeId: slot.routeCodeId,
|
||||
isPremiumAppointment: slot.isPremiumAppointment,
|
||||
},
|
||||
appointmentType: AppointmentTypeStrings.MOBILE,
|
||||
providerNumber: this.providerNumber,
|
||||
routeCodeId: slot.routeCodeId,
|
||||
isPremiumAppointment: slot.isPremiumAppointment,
|
||||
});
|
||||
},
|
||||
addPremiumFlagToInput(routeCode) {
|
||||
|
|
@ -300,11 +313,11 @@ export default {
|
|||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
transform: rotate(90deg);
|
||||
transform: rotate(180deg);
|
||||
transition: transform 150ms linear;
|
||||
|
||||
&--expanded {
|
||||
transform: rotate(-90deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,12 +22,24 @@
|
|||
<mobileSchedulingCard
|
||||
v-if="showMobileSchedulingCard"
|
||||
class="mt-4"
|
||||
v-model="selectedMobileScheduling"
|
||||
v-model="selectedScheduling"
|
||||
:providerNumber="mobileProviderAndTimeSlot.providerNumber"
|
||||
:timeSlots="mobileTimeSlotsForSelectedDate"
|
||||
:premiumTimeSlotPrice="premiumTimeSlotPrice"
|
||||
:zipCode="serviceZipCode"
|
||||
:showFreeFlag="showMobileFreeFlag"
|
||||
:radioGroupName="schedulingRadioGroupName"
|
||||
@zip-code-clicked="onMobileZipCodeClicked" />
|
||||
<inshopSchedulingCard
|
||||
v-for="{ provider } in inShopProvidersAndTimeslots"
|
||||
v-show="selectedDate"
|
||||
:key="provider.providerNumber"
|
||||
class="mt-4"
|
||||
v-model="selectedScheduling"
|
||||
:provider="provider"
|
||||
:timeSlots="getInshopTimeSlotsForSelectedDate(provider.providerNumber)"
|
||||
:radioGroupName="schedulingRadioGroupName"
|
||||
@address-clicked="onInshopAddressClicked(provider)" />
|
||||
<navbar
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
ref="navbar"
|
||||
|
|
@ -46,6 +58,7 @@ import navbar from "@/fmg-components/nav-bar/nav-bar";
|
|||
import { Form } from "vee-validate";
|
||||
import datePicker from "@/layouts/scheduling/date-picker/date-picker";
|
||||
import mobileSchedulingCard from "@/layouts/scheduling/mobile-scheduling-card/mobile-scheduling-card";
|
||||
import inshopSchedulingCard from "@/layouts/scheduling/inshop-scheduling-card/inshop-scheduling-card";
|
||||
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
||||
import store from "@/store";
|
||||
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
|
||||
|
|
@ -73,6 +86,7 @@ function toDateString(offsetDays, base = new Date()) {
|
|||
}
|
||||
|
||||
const SCHEDULE_FETCH_DAYS = 15;
|
||||
const SCHEDULING_RADIO_GROUP_NAME = "schedulingTimeSlot";
|
||||
|
||||
/**
|
||||
* Returns a promise for inshop time slots for a single provider.
|
||||
|
|
@ -171,10 +185,13 @@ export default {
|
|||
},
|
||||
watch: {
|
||||
selectedDate() {
|
||||
this.selectedMobileScheduling = null;
|
||||
this.selectedScheduling = null;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
schedulingRadioGroupName() {
|
||||
return SCHEDULING_RADIO_GROUP_NAME;
|
||||
},
|
||||
serviceLocationText() {
|
||||
return this.getCmsContent("ServiceLocationText", "Text");
|
||||
},
|
||||
|
|
@ -223,10 +240,24 @@ export default {
|
|||
inShopProvidersAndTimeslots: [],
|
||||
mobileProviderAndTimeSlot: null,
|
||||
mobilePremiumAppointmentFee: null,
|
||||
selectedMobileScheduling: null,
|
||||
selectedScheduling: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getInshopTimeSlotsForSelectedDate(providerNumber) {
|
||||
if (!this.selectedDate) {
|
||||
return [];
|
||||
}
|
||||
const providerEntry = this.inShopProvidersAndTimeslots.find(
|
||||
({ provider }) => provider.providerNumber === providerNumber
|
||||
);
|
||||
const day = providerEntry?.timeSlots?.days?.find((d) => d.date === this.selectedDate);
|
||||
return day?.timeSlots ?? [];
|
||||
},
|
||||
onInshopAddressClicked(provider) {
|
||||
// TODO: open Google Maps when address link functionality is implemented
|
||||
console.log("onInshopAddressClicked", provider?.providerNumber);
|
||||
},
|
||||
onMobileZipCodeClicked() {
|
||||
// TODO: open service zip modal when zip edit is implemented for scheduling page
|
||||
},
|
||||
|
|
@ -346,6 +377,7 @@ export default {
|
|||
Form,
|
||||
datePicker,
|
||||
mobileSchedulingCard,
|
||||
inshopSchedulingCard,
|
||||
loadingModal,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
placeholderText="MM/DD/YYYY"
|
||||
validationRules="date-of-birth-required"
|
||||
format="MM/dd/yyyy"
|
||||
modelType="MM/dd/yyyy"
|
||||
modelType="yyyy-MM-dd"
|
||||
:enableTimePicker="false"
|
||||
:textInputOnly="true"
|
||||
:maxDate="today"
|
||||
|
|
@ -55,7 +55,7 @@
|
|||
placeholderText="MM/DD/YYYY"
|
||||
validationRules="date-of-loss-required"
|
||||
format="MM/dd/yyyy"
|
||||
modelType="MM/dd/yyyy"
|
||||
modelType="yyyy-MM-dd"
|
||||
:enableTimePicker="false"
|
||||
:textInput="true"
|
||||
:maxDate="today"
|
||||
|
|
|
|||
|
|
@ -114,6 +114,13 @@ const getDefaultState = () => {
|
|||
phoneNumber: null,
|
||||
isSmsOptIn: null,
|
||||
waitListRequested: null,
|
||||
address: {
|
||||
streetAddress: null,
|
||||
streetAddress2: null,
|
||||
city: null,
|
||||
state: null,
|
||||
zipCode: null,
|
||||
},
|
||||
},
|
||||
damage: {
|
||||
isRepair: null,
|
||||
|
|
@ -141,6 +148,7 @@ const getDefaultState = () => {
|
|||
coverageSubStatus: null,
|
||||
coverageType: null,
|
||||
coverageVerificationType: null,
|
||||
claimNumber: null,
|
||||
},
|
||||
parentAccountNumber: 0,
|
||||
accountName: null,
|
||||
|
|
@ -171,20 +179,12 @@ const getDefaultState = () => {
|
|||
currentDeductible: 0,
|
||||
originalDeductible: 0,
|
||||
policyNumber: null,
|
||||
claimNumber: null,
|
||||
isItac: false,
|
||||
additionalAuthFlag: null,
|
||||
isNoComp: false,
|
||||
insuranceCompanyName: null,
|
||||
phoneNumber: null,
|
||||
phoneExtension: null,
|
||||
policyAddress: {
|
||||
streetAddress: null,
|
||||
streetAddress2: null,
|
||||
city: null,
|
||||
state: null,
|
||||
zipCode: null,
|
||||
},
|
||||
dateOfBirth: null,
|
||||
lossTime: null,
|
||||
lossLocation: null,
|
||||
|
|
@ -363,22 +363,22 @@ export const mutations = {
|
|||
state.order.policy.phoneExtension = phoneExtension;
|
||||
},
|
||||
updateStreetAddress(state, streetAddress) {
|
||||
state.order.policy.policyAddress.streetAddress = streetAddress;
|
||||
state.order.customer.address.streetAddress = streetAddress;
|
||||
},
|
||||
updateStreetAddress2(state, streetAddress2) {
|
||||
state.order.policy.policyAddress.streetAddress2 = streetAddress2;
|
||||
state.order.customer.address.streetAddress2 = streetAddress2;
|
||||
},
|
||||
updateCity(state, city) {
|
||||
state.order.policy.policyAddress.city = city;
|
||||
state.order.customer.address.city = city;
|
||||
},
|
||||
updateState(state, stateValue) {
|
||||
state.order.policy.policyAddress.state = stateValue;
|
||||
state.order.customer.address.state = stateValue;
|
||||
},
|
||||
updateZipCode(state, zipCode) {
|
||||
state.order.policy.policyAddress.zipCode = zipCode;
|
||||
state.order.customer.address.zipCode = zipCode;
|
||||
},
|
||||
updateClaimNumber(state, claimNumber) {
|
||||
state.order.policy.claimNumber = claimNumber;
|
||||
state.order.payment.insuranceCoverage.claimNumber = claimNumber;
|
||||
},
|
||||
updateIsClaimAndCoverage(state, isClaimAndCoverage) {
|
||||
state.order.payment.isClaimAndCoverage = isClaimAndCoverage;
|
||||
|
|
@ -405,13 +405,13 @@ export const mutations = {
|
|||
};
|
||||
|
||||
state.order.policy.policyNumber = null;
|
||||
state.order.policy.claimNumber = null;
|
||||
state.order.payment.insuranceCoverage.claimNumber = null;
|
||||
state.order.policy.insuranceCompanyName = null;
|
||||
state.order.policy.policyAddress.streetAddress = null;
|
||||
state.order.policy.policyAddress.streetAddress2 = null;
|
||||
state.order.policy.policyAddress.city = null;
|
||||
state.order.policy.policyAddress.state = null;
|
||||
state.order.policy.policyAddress.zipCode = null;
|
||||
state.order.customer.address.streetAddress = null;
|
||||
state.order.customer.address.streetAddress2 = null;
|
||||
state.order.customer.address.city = null;
|
||||
state.order.customer.address.state = null;
|
||||
state.order.customer.address.zipCode = null;
|
||||
state.order.damage.dateOfLoss = null;
|
||||
state.order.damage.damageCause = null;
|
||||
},
|
||||
|
|
@ -565,12 +565,12 @@ export const mutations = {
|
|||
updateInsuranceDetails(state, insuranceDetails) {
|
||||
if (insuranceDetails) {
|
||||
state.order.policy.policyNumber = insuranceDetails.policyNumber;
|
||||
state.order.policy.policyAddress.streetAddress = insuranceDetails.streetAddress;
|
||||
state.order.policy.policyAddress.streetAddress2 = insuranceDetails.streetAddress2;
|
||||
state.order.policy.policyAddress.city = insuranceDetails.city;
|
||||
state.order.policy.policyAddress.state = insuranceDetails.state;
|
||||
state.order.policy.policyAddress.zipCode = insuranceDetails.zipCode;
|
||||
state.order.policy.claimNumber = insuranceDetails.claimNumber;
|
||||
state.order.customer.address.streetAddress = insuranceDetails.streetAddress;
|
||||
state.order.customer.address.streetAddress2 = insuranceDetails.streetAddress2;
|
||||
state.order.customer.address.city = insuranceDetails.city;
|
||||
state.order.customer.address.state = insuranceDetails.state;
|
||||
state.order.customer.address.zipCode = insuranceDetails.zipCode;
|
||||
state.order.payment.insuranceCoverage.claimNumber = insuranceDetails.claimNumber;
|
||||
state.order.policy.phoneNumber = insuranceDetails.phoneNumber;
|
||||
state.order.policy.phoneExtension = insuranceDetails.phoneExtension;
|
||||
state.order.policy.morePolicyQuestions = insuranceDetails.morePolicyQuestions;
|
||||
|
|
@ -587,7 +587,6 @@ export const mutations = {
|
|||
state.order.payment.insuranceCoverage.coverageSubStatus =
|
||||
insuranceDetails.coverageSubStatus;
|
||||
}
|
||||
console.log("::::" + state.order.payment.insuranceCoverage.coverageStatus);
|
||||
},
|
||||
updateDamageDetails(state, damageDetails) {
|
||||
if (damageDetails) {
|
||||
|
|
@ -958,6 +957,16 @@ export const mutations = {
|
|||
state.order.customer.phoneNumber = sessionInformation.order.customer.homePhone;
|
||||
state.order.customer.isSmsOptIn = sessionInformation.order.customer.isSmsOptIn;
|
||||
|
||||
if (sessionInformation.order.customer.address) {
|
||||
state.order.customer.address = Object.assign(state.order.customer.address, {
|
||||
streetAddress: sessionInformation.order.customer.address.streetAddress,
|
||||
streetAddress2: sessionInformation.order.customer.address.streetAddress2,
|
||||
city: sessionInformation.order.customer.address.city,
|
||||
state: sessionInformation.order.customer.address.state,
|
||||
zipCode: sessionInformation.order.customer.address.zipCode,
|
||||
});
|
||||
}
|
||||
|
||||
state.applicationUser.experiments = sessionInformation.applicationUser.experiments;
|
||||
state.applicationUser.crmCustomerId = sessionInformation.applicationUser.crmCustomerId;
|
||||
state.applicationUser.pageData = sessionInformation.applicationUser.pageData;
|
||||
|
|
@ -985,20 +994,11 @@ export const mutations = {
|
|||
? true
|
||||
: false;
|
||||
state.order.policy.policyNumber = sessionInformation.order.policy?.policyNumber;
|
||||
state.order.policy.claimNumber = sessionInformation.order.policy?.claimNumber;
|
||||
state.order.payment.insuranceCoverage.claimNumber =
|
||||
sessionInformation.order.insuranceCoverage?.claimNumber;
|
||||
state.order.policy.insuranceCompanyName =
|
||||
sessionInformation.order.policy?.insuranceCompanyName;
|
||||
|
||||
if (!state.order.policy.policyAddress) {
|
||||
state.order.policy.policyAddress = {};
|
||||
}
|
||||
const sessionPolicyAddress = sessionInformation.order.policy?.address;
|
||||
state.order.policy.policyAddress.streetAddress = sessionPolicyAddress?.streetAddress;
|
||||
state.order.policy.policyAddress.streetAddress2 = sessionPolicyAddress?.streetAddress2;
|
||||
state.order.policy.policyAddress.city = sessionPolicyAddress?.city;
|
||||
state.order.policy.policyAddress.state = sessionPolicyAddress?.state;
|
||||
state.order.policy.policyAddress.zipCode = sessionPolicyAddress?.zipCode;
|
||||
|
||||
state.order.policy.isNoComp =
|
||||
coverageTypeValue(sessionInformation?.order.insuranceCoverage.coverageType) ===
|
||||
coverageType.NOCOMP
|
||||
|
|
@ -2668,6 +2668,13 @@ export const actions = {
|
|||
lastName: order.customer.lastName,
|
||||
isSmsOptIn: order.customer.isSmsOptIn,
|
||||
phoneNumber: order.customer.phoneNumber,
|
||||
address: {
|
||||
streetAddress: order.customer.address.streetAddress,
|
||||
streetAddress2: order.customer.address.streetAddress2,
|
||||
city: order.customer.address.city,
|
||||
state: order.customer.address.state,
|
||||
zipCode: order.customer.address.zipCode,
|
||||
},
|
||||
},
|
||||
damage: {
|
||||
numberOfChips: damage.numberOfChips,
|
||||
|
|
@ -2695,6 +2702,7 @@ export const actions = {
|
|||
coverageVerificationType:
|
||||
order.payment.insuranceCoverage.coverageVerificationType,
|
||||
coverageSubStatus: order.payment.insuranceCoverage.coverageSubStatus,
|
||||
claimNumber: order.payment.insuranceCoverage.claimNumber,
|
||||
},
|
||||
isInsurance: order.payment.isInsurance,
|
||||
parentAccountNumber: order.payment.parentAccountNumber,
|
||||
|
|
@ -2730,15 +2738,7 @@ export const actions = {
|
|||
isNoComp: order.policy?.isNoComp,
|
||||
currentDeductible: order.policy?.currentDeductible,
|
||||
originalDeductible: order.policy?.originalDeductible,
|
||||
policyAddress: {
|
||||
streetAddress: order.policy?.policyAddress?.streetAddress,
|
||||
streetAddress2: order.policy?.policyAddress?.streetAddress2,
|
||||
city: order.policy?.policyAddress?.city,
|
||||
state: order.policy?.policyAddress?.state,
|
||||
zipCode: order.policy?.policyAddress?.zipCode,
|
||||
},
|
||||
policyNumber: order.policy?.policyNumber,
|
||||
claimNumber: order.policy?.claimNumber,
|
||||
},
|
||||
serviceLocation: {
|
||||
streetAddress: order.serviceLocation.address,
|
||||
|
|
|
|||
|
|
@ -765,7 +765,20 @@ describe("Actions", () => {
|
|||
},
|
||||
},
|
||||
serviceLocation: {},
|
||||
customer: {},
|
||||
customer: {
|
||||
emailAddress: "test@safelite.com",
|
||||
firstName: "John",
|
||||
lastName: "Doe",
|
||||
isSmsOptIn: true,
|
||||
phoneNumber: "1234567890",
|
||||
address: {
|
||||
streetAddress: "123 Main St",
|
||||
streetAddress2: "Apt 1",
|
||||
city: "Anytown",
|
||||
state: "OH",
|
||||
zipCode: "12345",
|
||||
},
|
||||
},
|
||||
lineItems: {},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue