Merge 488 into 530
Refactors the date picker component to support pricing by day experiment, including new styling and logic for displaying prices. Adds a progress bar to the funnel header to visually represent the user's progress through the workflow. Updates dependencies including axios, and adds axios mock adapter and retry. Adds new icons and updates styling for various UI components. Improves logic for handling heavy trucks and closest shops.
This commit is contained in:
commit
ac53601a34
28 changed files with 912 additions and 1285 deletions
135
package-lock.json
generated
135
package-lock.json
generated
|
|
@ -9,7 +9,6 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@iframe-resizer/child": "^5.3.3",
|
||||
"axios": "^0.23.0",
|
||||
"bootstrap": "^5.3.3",
|
||||
"core-js": "^3.38.1",
|
||||
"http-status-codes": "^2.3.0",
|
||||
|
|
@ -38,6 +37,9 @@
|
|||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vue/vue3-jest": "^27.0.0",
|
||||
"axios": "^1.8.4",
|
||||
"axios-mock-adapter": "^2.1.0",
|
||||
"axios-retry": "^4.5.0",
|
||||
"dotenv-safe": "^9.1.0",
|
||||
"eslint": "8.57",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
|
|
@ -5412,12 +5414,82 @@
|
|||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.23.0.tgz",
|
||||
"integrity": "sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==",
|
||||
"version": "1.8.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
|
||||
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.4"
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios-mock-adapter": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-2.1.0.tgz",
|
||||
"integrity": "sha512-AZUe4OjECGCNNssH8SOdtneiQELsqTsat3SQQCWLPjN436/H+L9AjWfV7bF+Zg/YL9cgbhrz5671hoh+Tbn98w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"is-buffer": "^2.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"axios": ">= 0.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios-mock-adapter/node_modules/is-buffer": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
|
||||
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/axios-retry": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz",
|
||||
"integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"is-retry-allowed": "^2.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"axios": "0.x || 1.x"
|
||||
}
|
||||
},
|
||||
"node_modules/axios/node_modules/form-data": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
|
|
@ -8005,6 +8077,22 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
||||
|
|
@ -9227,6 +9315,7 @@
|
|||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
|
|
@ -9694,6 +9783,22 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-unicode": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
||||
|
|
@ -10509,6 +10614,19 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-retry-allowed": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz",
|
||||
"integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
|
||||
|
|
@ -16247,6 +16365,13 @@
|
|||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pseudomap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@
|
|||
"prettier": "^3.3.3",
|
||||
"sass": "^1.77.8",
|
||||
"sass-loader": "^8.0.2",
|
||||
"sqlite": "^5.1.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"eslintConfig": {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,13 @@
|
|||
# Environment type
|
||||
NODE_ENV="qa"
|
||||
|
||||
SKIP_CONTENT_SITE=false
|
||||
|
||||
# Base URLs by environment (uncomment the one you need)
|
||||
# qa
|
||||
BASE_URL="https://www-qa2.safelite.com/"
|
||||
# qa with skipToInsurance Turned Off
|
||||
# BASE_URL="https://fixmyglassqa.safelite.com/?cns=all&experiments=ConceptFunnel=ConceptFunnel_V1=ConceptFunnel_VinAndEmailOptional=true,NextGen_IGQSkipToInsurance=NextGen_IGQSkipToInsurance_V1=NextGen_IGQSkipToInsurance_CONTROL=true"
|
||||
# sys
|
||||
# BASE_URL="https://www-test2.safelite.com/fmg/?fmgPage=vehicle"
|
||||
# dev
|
||||
|
|
|
|||
|
|
@ -1,11 +1,3 @@
|
|||
schedules:
|
||||
- cron: 0 9 * * MON-FRI
|
||||
always: true
|
||||
displayName: Daily Test Automation Run for FMG-NextGen
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
|
||||
pool: 'AmazonLinuxPool'
|
||||
|
||||
variables:
|
||||
|
|
@ -50,10 +42,10 @@ stages:
|
|||
container_id=$(docker create \
|
||||
--ipc=host \
|
||||
-e CCIS_API_AUTH=$(CCIS_API_AUTH) \
|
||||
-e BASE_URL=$(BASE_URL) \
|
||||
-e BASE_URL="$(BASE_URL)" \
|
||||
-e CCIS_API_URL=$(CCIS_API_URL) \
|
||||
-e ADMIN_SERVICE_API_URL=$(ADMIN_SERVICE_API_URL) \
|
||||
-e SHARD=$(shardNumber) \
|
||||
-e SKIP_CONTENT_SITE=$(SKIP_CONTENT_SITE) \
|
||||
-e CI=true \
|
||||
-e NODE_ENV=$(NODE_ENV) \
|
||||
$(dockerImageName):$(imageTag) \
|
||||
|
|
@ -131,6 +123,7 @@ stages:
|
|||
-e REPORT_NAME="$report_name" \
|
||||
-e CURRENT_DAY="$current_day" \
|
||||
-e NUMERIC_DATE="$numeric_date" \
|
||||
-e NODE_ENV=$(NODE_ENV) \
|
||||
$(dockerImageName):$(imageTag) \
|
||||
bash -c "chmod +x devops/scripts/jira_writeback.sh
|
||||
echo \"Moving Playwright reports out of subfolders...\"
|
||||
|
|
@ -151,7 +144,7 @@ stages:
|
|||
fi
|
||||
|
||||
echo \"Writing report to Jira card '$jira_card_number'...\"
|
||||
/app/devops/scripts/jira_writeback.sh add_comment \"$jira_card_number\" /app/ortoni-report/\${REPORT_NAME} \"AUTOMATED TEST RUN: $(date) - \${CURRENT_DAY} (\${NUMERIC_DATE})\" ")
|
||||
/app/devops/scripts/jira_writeback.sh add_comment \"$jira_card_number\" /app/ortoni-report/\${REPORT_NAME} \"AUTOMATED TEST RUN: $(date) - \${CURRENT_DAY} (\${NUMERIC_DATE}) - Environment: \${NODE_ENV}\" ")
|
||||
|
||||
# Start container and stream logs
|
||||
echo "Starting merge"
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export function getCustomerDetails(): ICustomerDetails {
|
|||
street: faker.location.streetAddress(),
|
||||
city: 'Columbus',
|
||||
state: 'Ohio',
|
||||
postalCode: '43215',
|
||||
postalCode: '21237',
|
||||
country: 'United States'
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ export class ServicePackagesPage extends BasePage {
|
|||
if (await this.repeatedClicksModalCloseButton.isVisible()) {
|
||||
await this.repeatedClicksModalCloseButton.click();
|
||||
}
|
||||
await this.page.waitForTimeout(3000);
|
||||
await this.closeButton.click();
|
||||
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -168,8 +168,9 @@ async function runWorkflow(page: Page, testCase: TestCase) {
|
|||
|
||||
//============================= TEST WORKFLOW STEPS =============================
|
||||
|
||||
// Execute home page for qa and dev environments (skip for sys)
|
||||
if (process.env.SKIP_CONTENT_SITE == "false") {
|
||||
// Use Environment Variable to decide whether or not we want to skip content site aka home page
|
||||
|
||||
if (process.env.SKIP_CONTENT_SITE == "false") {
|
||||
await test.step('HomePage >> Lets Get Started', async () => {
|
||||
let homePage = testCase.pages.homePage;
|
||||
console.log(`Customer for this test: ${customerDetails?.firstName} ${customerDetails?.lastName}`);
|
||||
|
|
|
|||
6
src/assets/img/icons/calendar-icon-black.svg
Normal file
6
src/assets/img/icons/calendar-icon-black.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.65567 6.44287H13.35" stroke="#0A0A0A" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.4256 2V3.97449" stroke="#0A0A0A" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5.57864 2V3.97449" stroke="#0A0A0A" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5427 2.94792H5.4625C3.70011 2.94792 2.60001 3.92933 2.60001 5.73323V11.1637C2.60001 12.9962 3.70011 14.0003 5.4625 14.0003H10.5375C12.3051 14.0003 13.4001 13.0137 13.4001 11.2091V5.73323C13.4052 3.92933 12.3103 2.94792 10.5427 2.94792Z" stroke="#0A0A0A" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 748 B |
6
src/assets/img/icons/calendar-icon-grey.svg
Normal file
6
src/assets/img/icons/calendar-icon-grey.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.15567 6.44287H13.85" stroke="#4B4C4E" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.9256 2V3.97449" stroke="#4B4C4E" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.07864 2V3.97449" stroke="#4B4C4E" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.0427 2.94791H5.9625C4.20011 2.94791 3.10001 3.92931 3.10001 5.73321V11.1637C3.10001 12.9962 4.20011 14.0003 5.9625 14.0003H11.0375C12.8051 14.0003 13.9001 13.0137 13.9001 11.2091V5.73321C13.9052 3.92931 12.8103 2.94791 11.0427 2.94791Z" stroke="#4B4C4E" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 748 B |
4
src/assets/img/icons/info-circle.svg
Normal file
4
src/assets/img/icons/info-circle.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 12C3 16.9699 7.02908 21 12 21C16.9709 21 21 16.9699 21 12C21 7.02908 16.9709 3 12 3C7.02908 3 3 7.02908 3 12Z" stroke="#0070D1" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12.0057 15.6932V11.3936M12 8.35426V8.29102" stroke="#0070D1" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 450 B |
6
src/assets/img/icons/payment-icon-green.svg
Normal file
6
src/assets/img/icons/payment-icon-green.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 13.8143H13.5894" stroke="#00723E" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.20013 2.88802L3.08796 7.00022C2.09364 7.99456 2.17407 9.32289 3.17099 10.3198L5.86013 13.0096C6.85706 14.0065 8.18213 14.0896 9.18039 13.092L13.2919 8.98042C14.2901 7.98222 14.2065 6.6571 13.2095 5.66018L10.5197 2.97039C9.52286 1.97348 8.19773 1.89046 7.20013 2.88802Z" stroke="#00723E" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3.6315 10.782L10.9809 3.4326" stroke="#00723E" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.51559 11.2542L8.72786 10.042" stroke="#00723E" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 800 B |
6
src/assets/img/icons/payment-icon-grey.svg
Normal file
6
src/assets/img/icons/payment-icon-grey.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.5 13.8143H14.0894" stroke="#4B4C4E" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.70014 2.88802L3.58797 7.00022C2.59365 7.99456 2.67408 9.32289 3.671 10.3198L6.36014 13.0096C7.35707 14.0065 8.68214 14.0896 9.68041 13.092L13.7919 8.98042C14.7901 7.98222 14.7065 6.6571 13.7095 5.66018L11.0197 2.97039C10.0229 1.97348 8.69774 1.89046 7.70014 2.88802Z" stroke="#4B4C4E" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.13152 10.782L11.4809 3.43262" stroke="#4B4C4E" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.01558 11.2543L9.22785 10.042" stroke="#4B4C4E" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 802 B |
26
src/constants/progress-bar-mapper.js
Normal file
26
src/constants/progress-bar-mapper.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
const pagePercentageMapper = {
|
||||
vehicle: 4,
|
||||
"vehicle-damage": 16,
|
||||
estimate: 28,
|
||||
"service-zip": 32,
|
||||
"vin-lookup": 32,
|
||||
"license-plate-lookup": 32,
|
||||
"address-lookup": 32,
|
||||
"address-vehicles": 36,
|
||||
"part-questions": 40,
|
||||
"molding-questions": 40,
|
||||
"vehicle-parts": 40,
|
||||
"capability-questions": 40,
|
||||
quote: 48,
|
||||
"insurance-company": 52,
|
||||
"service-location": 60,
|
||||
schedule: 72,
|
||||
"customer-details": 84,
|
||||
"payment-method": 92,
|
||||
payment: 96,
|
||||
confirmation: 100,
|
||||
};
|
||||
|
||||
export const getProgressBarPercentage = (page) => {
|
||||
return pagePercentageMapper[page] || 0;
|
||||
};
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
<template>
|
||||
<div class="date-picker text-center" :class="calendarViewDirection">
|
||||
<div
|
||||
class="date-picker text-center"
|
||||
:class="`${calendarViewDirection} ${isPricingByDayClass} ${showPricingByDayClass}`">
|
||||
<fieldset id="date-picker-fieldset" ref="datePickerFieldset">
|
||||
<legend class="sr-only">Select a day and time</legend>
|
||||
<div
|
||||
|
|
@ -60,18 +62,24 @@
|
|||
date.dateNum === 1 ? 'first-day-' + month.startDateDayIndex : '',
|
||||
date.dayClasses,
|
||||
date.isSelectable ? 'selectable-day' : '',
|
||||
date.isPricingByDayUpchargeDay ? 'upch-day' : '',
|
||||
]">
|
||||
<input
|
||||
:disabled="!date.isSelectable"
|
||||
type="radio"
|
||||
name="day-of-month"
|
||||
v-model="selectedDate"
|
||||
@click="fireDateSelectedEvent"
|
||||
@keypress.enter="fireDateSelectedEvent"
|
||||
@click="fireDateSelectedEvent($event, date)"
|
||||
@keypress.enter="fireDateSelectedEvent($event, date)"
|
||||
:value="date.inputValue"
|
||||
:id="`${month.monthLabel}-${date.dateNum.toString()}`" />
|
||||
<label :for="`${month.monthLabel}-${date.dateNum.toString()}`">
|
||||
<span>{{ date.dateNum.toString() }}</span>
|
||||
<span
|
||||
v-if="isPricingByDayExperiment && showPricingByDay && date.isSelectable"
|
||||
class="price">
|
||||
{{ date.priceString }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -99,8 +107,17 @@
|
|||
// Supporting files
|
||||
import loader from "@/ux-components/loader/loader";
|
||||
import store from "@/store";
|
||||
import { TIMINGFUNC_MAP, BUFFER_OFFSET, MONTHS_OF_YEAR } from "./mixins/constants";
|
||||
import { selectableDaysOptions, requiredParameter, forceTwoDigitString } from "./mixins/helpers";
|
||||
import {
|
||||
TIMINGFUNC_MAP,
|
||||
BUFFER_OFFSET,
|
||||
MONTHS_OF_YEAR,
|
||||
DAYS_OF_WEEK,
|
||||
} from "@/digital-components/date-picker/mixins/constants";
|
||||
import {
|
||||
selectableDaysOptions,
|
||||
requiredParameter,
|
||||
forceTwoDigitString,
|
||||
} from "@/digital-components/date-picker/mixins/helpers";
|
||||
import {
|
||||
convertDateToDateString,
|
||||
convertDateStringToDate,
|
||||
|
|
@ -148,6 +165,10 @@ export default {
|
|||
type: String,
|
||||
default: "",
|
||||
},
|
||||
showPricingByDay: Boolean,
|
||||
pricingByDayBasePrice: Number,
|
||||
pricingByDayUpcharge: Number,
|
||||
isPricingByDayExperiment: Boolean,
|
||||
},
|
||||
setup(props) {
|
||||
const uuid = uuidv4();
|
||||
|
|
@ -199,6 +220,12 @@ export default {
|
|||
if (this.selectableDatesSetting === "custom") return "future";
|
||||
return "past";
|
||||
},
|
||||
isPricingByDayClass() {
|
||||
return this.isPricingByDayExperiment ? "pricing-by-day" : "";
|
||||
},
|
||||
showPricingByDayClass() {
|
||||
return this.showPricingByDay ? "show-pricing-by-day" : "";
|
||||
},
|
||||
selectedDate: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
|
|
@ -212,12 +239,12 @@ export default {
|
|||
initializeComponent(initialData) {
|
||||
this.setCalendarData(initialData);
|
||||
},
|
||||
fireDateSelectedEvent(event) {
|
||||
fireDateSelectedEvent(event, date) {
|
||||
// Ignore if arrow key selected radioButton
|
||||
if (event.screenX === 0 && event.screenY === 0) {
|
||||
return;
|
||||
}
|
||||
this.$emit("date-clicked");
|
||||
this.$emit("date-clicked", date);
|
||||
},
|
||||
getWeekStartDate(dateString) {
|
||||
const date = convertDateStringToDate(dateString);
|
||||
|
|
@ -440,6 +467,8 @@ export default {
|
|||
initialViewEndDate: config.initialViewEndDate,
|
||||
hideSecondMonth: hideSecondMonth,
|
||||
preSelectedDate: config.preSelectedDate,
|
||||
pricingByDayBasePrice: config.pricingByDayBasePrice,
|
||||
pricingByDayUpcharge: config.pricingByDayUpcharge,
|
||||
};
|
||||
if (direction === "future") {
|
||||
// first 0, then 1
|
||||
|
|
@ -573,6 +602,17 @@ export default {
|
|||
("0" + monthNum).slice(-2) +
|
||||
"-" +
|
||||
("0" + i).slice(-2);
|
||||
const dayIndex = convertDateStringToDate(dateString).getDay();
|
||||
const dayObject = DAYS_OF_WEEK[dayIndex];
|
||||
const isSelectable =
|
||||
this.selectableDatesData.findIndex((date) => date.date === dateString) > -1
|
||||
? true
|
||||
: false;
|
||||
const isPricingByDayUpchargeDay = dayObject.isPricingByDayUpchargeDay;
|
||||
const displayPrice = isPricingByDayUpchargeDay
|
||||
? options.pricingByDayBasePrice + options.pricingByDayUpcharge
|
||||
: options.pricingByDayBasePrice;
|
||||
const priceString = "$" + displayPrice;
|
||||
|
||||
if (offset === 0 && i === this.todayDateNum) {
|
||||
dayClasses += " current-day";
|
||||
|
|
@ -583,8 +623,8 @@ export default {
|
|||
if (offset === 0 && i > this.todayDateNum && calendarViewDirection === "past") {
|
||||
dayClasses += " unavailable-day";
|
||||
}
|
||||
if (convertDateStringToDate(dateString).getDay() === 0) {
|
||||
dayClasses += " sunday";
|
||||
if (dayIndex === 0) {
|
||||
dayClasses += " " + dayObject.cssClass;
|
||||
}
|
||||
if (
|
||||
this.hideSomeDaysForInitialView &&
|
||||
|
|
@ -598,10 +638,9 @@ export default {
|
|||
dateNum: i,
|
||||
dayClasses: dayClasses,
|
||||
inputValue: dateString,
|
||||
isSelectable:
|
||||
this.selectableDatesData.findIndex((date) => date.date === dateString) > -1
|
||||
? true
|
||||
: false,
|
||||
priceString: priceString,
|
||||
isSelectable: isSelectable,
|
||||
isPricingByDayUpchargeDay: isPricingByDayUpchargeDay,
|
||||
};
|
||||
dates.push(dateObject);
|
||||
}
|
||||
|
|
@ -897,7 +936,6 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:checked {
|
||||
@include media-breakpoint-up(sm) {
|
||||
box-shadow: 0 0 0 4px transparent;
|
||||
|
|
@ -924,9 +962,7 @@ export default {
|
|||
|
||||
&.selectable-day {
|
||||
label {
|
||||
color: $blue;
|
||||
background-color: $blue-100;
|
||||
border: 1px solid $blue;
|
||||
min-width: 2.5rem;
|
||||
width: 2.5rem;
|
||||
border-radius: 50%;
|
||||
|
|
@ -934,12 +970,11 @@ export default {
|
|||
}
|
||||
&.unavailable-day {
|
||||
label {
|
||||
color: $gray-500;
|
||||
background-color: $gray-100;
|
||||
border: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
//NEEDED?
|
||||
&.unavailable-day:not(&.sunday) {
|
||||
label {
|
||||
&::before {
|
||||
|
|
@ -949,7 +984,6 @@ export default {
|
|||
left: -1rem;
|
||||
width: 1rem;
|
||||
height: 2.5rem;
|
||||
background: $gray-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1056,7 +1090,107 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
// pricing by day override styles
|
||||
&.pricing-by-day {
|
||||
.calendar-grid-container .grid-item {
|
||||
margin: 0 0 0.15rem 0;
|
||||
}
|
||||
.month-year {
|
||||
grid-area: 1 / 1 / 2 / 8;
|
||||
}
|
||||
.legend {
|
||||
display: none;
|
||||
}
|
||||
.radio-wrapper {
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
height: 2.5rem;
|
||||
color: $black;
|
||||
|
||||
input[type="radio"] {
|
||||
&:focus-visible + label {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:focus + label,
|
||||
&:checked:focus + label {
|
||||
box-shadow: none;
|
||||
background-color: $white;
|
||||
color: $black;
|
||||
}
|
||||
&:checked + label {
|
||||
background: $blue-100;
|
||||
border: 2px solid $blue;
|
||||
border-radius: 0.25rem;
|
||||
color: $black;
|
||||
span {
|
||||
font-family: AvertaSemibold;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
label {
|
||||
flex-direction: column;
|
||||
height: 2.5rem;
|
||||
width: 100%;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.2;
|
||||
justify-content: center;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
&.selectable-day {
|
||||
label {
|
||||
color: $black;
|
||||
background-color: $blue-100;
|
||||
min-width: 2.5rem;
|
||||
width: 2.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
}
|
||||
&.current-day {
|
||||
label:after {
|
||||
margin-top: 0.2rem;
|
||||
position: relative;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.show-pricing-by-day {
|
||||
.radio-wrapper {
|
||||
&.selectable-day label span {
|
||||
text-decoration: none;
|
||||
color: $black;
|
||||
font-weight: 400;
|
||||
font-family: "AvertaSemibold";
|
||||
|
||||
&.price {
|
||||
font-family: "AvertaRegular";
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
&.upch-day label span.price {
|
||||
color: $gray-600;
|
||||
font-weight: 400;
|
||||
font-family: $font-family-sans-serif;
|
||||
}
|
||||
input[type="radio"] {
|
||||
&:focus + label,
|
||||
&:checked:focus + label {
|
||||
color: $gray-600;
|
||||
}
|
||||
&:checked + label {
|
||||
color: $blue;
|
||||
span {
|
||||
font-weight: 400;
|
||||
font-family: "AvertaSemibold";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
display: block;
|
||||
position: relative;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -11,7 +11,6 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="footer-inner-wrapper">
|
||||
<p class="mx-3 my-0 pb-1">© 2025 Safelite Group</p>
|
||||
<textLink
|
||||
linkType="newWindowLink"
|
||||
text="Terms of service"
|
||||
|
|
@ -39,6 +38,7 @@
|
|||
href="https://www.safelite.com/ccpa-privacy-policy"
|
||||
target="_blank" />
|
||||
</div>
|
||||
<p class="mx-3 my-0 pb-1">© 2025 Safelite Group</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -60,8 +60,9 @@ export default {
|
|||
flex-direction: column;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
align-items: end;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
padding: 0 0 2rem 0;
|
||||
|
||||
.skyline {
|
||||
height: 96px;
|
||||
|
|
@ -70,30 +71,34 @@ export default {
|
|||
height: 48px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-inner-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-top: 1px solid $gray;
|
||||
padding: 2rem 0;
|
||||
padding: 2rem 0 0 0;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
font-size: 0.875rem;
|
||||
color: $black;
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
color: $gray-600;
|
||||
|
||||
:deep(a) {
|
||||
text-decoration: none;
|
||||
&.new-window-link {
|
||||
margin: 0 0.75rem;
|
||||
color: $gray-600;
|
||||
}
|
||||
&.navigation-link {
|
||||
margin: 0 0.75rem;
|
||||
color: $gray-600;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
<!-- // menu-modal temporarily not in use until the hamburger menu is re-implemented for use with the progress bar. See display: none below. -->
|
||||
<menuModal />
|
||||
<!-- // menu-modal temporarily not in use until the hamburger menu is re-implemented for use with the progress bar. See display: none below. -->
|
||||
<progressBar />
|
||||
</div>
|
||||
<template v-for="globalAlert in globalAlertMessages" :key="globalAlert.id">
|
||||
<transition name="fade" mode="out-in">
|
||||
|
|
@ -28,6 +29,7 @@ import eventBus from "@/helpers/event-bus/event-bus";
|
|||
import { globalEvents } from "@/constants/events";
|
||||
import menuModal from "@/fmg-components/funnel-header/menu-modal/menu-modal";
|
||||
import salesforceWebchat from "../../digital-components/salesforce-webchat/salesforce-webchat.vue";
|
||||
import progressBar from "@/fmg-components/funnel-header/progress-bar/progress-bar";
|
||||
|
||||
// Constants
|
||||
const ALERT_DURATION = 3000; // millisecond time to display alert before dismissal
|
||||
|
|
@ -68,6 +70,7 @@ export default {
|
|||
components: {
|
||||
alert,
|
||||
menuModal,
|
||||
progressBar,
|
||||
salesforceWebchat,
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import progressBar from "./progress-bar";
|
||||
import store from "@/store";
|
||||
|
||||
describe("progressBar", () => {
|
||||
test("progress should be 0", () => {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
const wrapper = shallowMount(progressBar, {
|
||||
mixins: [mockMixin],
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.progress).toBe(0);
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe("progressBar", () => {
|
||||
test("progress should be 4%", () => {
|
||||
// Arrange
|
||||
store.getters = {
|
||||
applicationUser: {
|
||||
lastPageVisited: "vehicle",
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
const wrapper = shallowMount(progressBar, {
|
||||
mixins: [mockMixin],
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.progress).toBe(4);
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe("progressBar", () => {
|
||||
test("progress should be 48%", () => {
|
||||
// Arrange
|
||||
store.getters = {
|
||||
applicationUser: {
|
||||
lastPageVisited: "quote",
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
const wrapper = shallowMount(progressBar, {
|
||||
mixins: [mockMixin],
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.progress).toBe(48);
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe("progressBar", () => {
|
||||
test("progress should be 100%", () => {
|
||||
// Arrange
|
||||
store.getters = {
|
||||
applicationUser: {
|
||||
lastPageVisited: "confirmation",
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
const wrapper = shallowMount(progressBar, {
|
||||
mixins: [mockMixin],
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.progress).toBe(100);
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
getProgress: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
<template>
|
||||
<div id="progress-bar-container">
|
||||
<progress v-if="progress > 0" :value="progress" max="100" v-html="progress + '%'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import store from "@/store";
|
||||
import { getProgressBarPercentage } from "@/constants/progress-bar-mapper";
|
||||
|
||||
export default {
|
||||
name: "progressBar",
|
||||
data() {
|
||||
return {
|
||||
progress: this.getProgress(),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getProgress() {
|
||||
var pageName = store.getters.applicationUser.lastPageVisited;
|
||||
return getProgressBarPercentage(pageName);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
#progress-bar-container {
|
||||
padding: 0.75rem 0;
|
||||
width: 100%;
|
||||
|
||||
progress {
|
||||
height: 10px;
|
||||
width: 100%;
|
||||
|
||||
/* Firefox */
|
||||
appearance: none;
|
||||
background-color: $blue-100;
|
||||
border: 0;
|
||||
border-radius: $border-radius-pill;
|
||||
box-shadow: $box-shadow-inset;
|
||||
|
||||
&::-moz-progress-bar {
|
||||
background-color: $blue;
|
||||
border-radius: $border-radius-pill;
|
||||
}
|
||||
|
||||
/* Webkit */
|
||||
-webkit-appearance: none;
|
||||
&::-webkit-progress-bar {
|
||||
background-color: $blue-100;
|
||||
border-radius: $border-radius-pill;
|
||||
box-shadow: $box-shadow-inset;
|
||||
}
|
||||
&::-webkit-progress-value {
|
||||
background-color: $blue;
|
||||
border-radius: $border-radius-pill;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -164,6 +164,8 @@ export default {
|
|||
emailOrSms: this.getEmailOrSmsFromStore(),
|
||||
},
|
||||
serviceZipCode: this.getServiceZipFromStore(),
|
||||
carId: this.getCarIdfromStore(),
|
||||
isHeavyTruck: this.getIsVehicleHeavyTruckServiceableFromStore(),
|
||||
displayNonServiceableZipAlert: false,
|
||||
displayVinNotFoundAlert: false,
|
||||
displayMatchedDifferentVehicleAlert: false,
|
||||
|
|
@ -219,6 +221,19 @@ export default {
|
|||
getServiceZipFromStore() {
|
||||
return this.$store.getters.order.serviceLocation.zipCode;
|
||||
},
|
||||
getCarIdfromStore() {
|
||||
return store.getters.vehicle.carId;
|
||||
},
|
||||
getIsVehicleHeavyTruckServiceableFromStore() {
|
||||
return store.getters.vehicle.isBigTruck && store.getters.vehicle.canSafeliteService;
|
||||
},
|
||||
async findClosestApplicableShops() {
|
||||
return await this.dispatchStoreActionWithLogging(
|
||||
storeActions.GET_CLOSEST_APPLICABLE_SHOPS,
|
||||
{ zip: this.serviceZipCode, carId: this.carId },
|
||||
"address-lookup"
|
||||
);
|
||||
},
|
||||
async forwardButtonAction() {
|
||||
this.resetWarningsAndErrors();
|
||||
|
||||
|
|
|
|||
|
|
@ -76,13 +76,20 @@
|
|||
:manualCopy="AlertMatchedDifferentVehicleBody"
|
||||
alertClass="alert-warning"
|
||||
v-bind:isDismissible="false" />
|
||||
<alert
|
||||
ref="AlertNoService"
|
||||
v-if="displayNoServiceAlert"
|
||||
class="my-4"
|
||||
cmsWidgetName="AlertNoServiceWidget"
|
||||
alertClass="alert-danger"
|
||||
v-bind:isDismissible="false" />
|
||||
|
||||
<navbar
|
||||
ref="navbar"
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
:isForwardActionDisabled="!meta.valid"
|
||||
@back-clicked="backButtonAction"
|
||||
@ForwardClicked="forwardButtonAction" />
|
||||
@ForwardClicked="forwardButtonAction || displayNoServiceAlert" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -158,6 +165,8 @@ export default {
|
|||
registrationZipCode: this.getRegistrationZipFromStore() ?? this.$route.query.zipcode,
|
||||
emailOrSms: this.getEmailOrSmsFromStore(),
|
||||
serviceZipCode: this.getServiceZipFromStore(),
|
||||
carId: this.getCarIdfromStore(),
|
||||
isHeavyTruck: this.getIsVehicleHeavyTruckServiceableFromStore(),
|
||||
displayNonServiceableZipAlert: false,
|
||||
displayVinNotFoundAlert: false,
|
||||
displayMatchedDifferentVehicleAlert: false,
|
||||
|
|
@ -200,6 +209,19 @@ export default {
|
|||
getServiceZipFromStore() {
|
||||
return this.$store.getters.order.serviceLocation.zipCode;
|
||||
},
|
||||
getCarIdfromStore() {
|
||||
return store.getters.vehicle.carId;
|
||||
},
|
||||
getIsVehicleHeavyTruckServiceableFromStore() {
|
||||
return store.getters.vehicle.isBigTruck && store.getters.vehicle.canSafeliteService;
|
||||
},
|
||||
async findClosestApplicableShops() {
|
||||
return await this.dispatchStoreActionWithLogging(
|
||||
storeActions.GET_CLOSEST_APPLICABLE_SHOPS,
|
||||
{ zip: this.serviceZipCode, carId: this.carId },
|
||||
"license-plate-lookup"
|
||||
);
|
||||
},
|
||||
async forwardButtonAction() {
|
||||
this.resetWarningsAndErrors();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,259 @@
|
|||
<template>
|
||||
<div class="alert fade show my-2 py-2 border-0 alert-info" role="alert">
|
||||
<component :is="'script'" src="https://js.afterpay.com/afterpay-1.x.js" async></component>
|
||||
<div class="my-4 alert-heading text-center">
|
||||
<span v-html="afterpayHeaderCopy" />
|
||||
|
||||
<span class="afterpay-amount">{{ this.afterpayPrice }}</span>
|
||||
<br />
|
||||
<span class="alert-sub-heading" v-html="afterpayHeaderSubHeaderCopy" />
|
||||
|
||||
<img :src="afterPayImageUrl" />
|
||||
|
||||
<a
|
||||
id="afterpay-learnmore"
|
||||
href="#"
|
||||
data-afterpay-modal="en_US-safelite"
|
||||
data-bind="click:afterpayLearnMore">
|
||||
<img :src="infoIcon" alt="Info Icon" class="info-icon" />
|
||||
</a>
|
||||
<div class="after-pay">
|
||||
<div
|
||||
class="after-pay-toggle"
|
||||
:class="[isExpanded ? 'expanded' : '']"
|
||||
@click="toggleIsExpanded">
|
||||
<a href="#!" v-html="paymentPlanToggleHeading" />
|
||||
</div>
|
||||
<div class="after-pay-details">
|
||||
<div class="payment-section">
|
||||
<div class="payment-card">
|
||||
<div class="calendar-section">
|
||||
<div class="icon"><img :src="calendarIcon" /></div>
|
||||
<div>Today</div>
|
||||
</div>
|
||||
<div class="payment-amount-section">
|
||||
<div class="icon"><img :src="paymentIcon" /></div>
|
||||
<div class="amount-due">{{ this.afterpayPrice }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="payment-card">
|
||||
<div class="calendar-section">
|
||||
<div class="icon"><img :src="calendarIcon" /></div>
|
||||
<div>2 weeks</div>
|
||||
</div>
|
||||
<div class="payment-amount-section">
|
||||
<div class="icon"><img :src="paymentIcon" /></div>
|
||||
<div class="amount-due">{{ this.afterpayPrice }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="payment-card">
|
||||
<div class="calendar-section">
|
||||
<div class="icon"><img :src="calendarIcon" /></div>
|
||||
<div>4 weeks</div>
|
||||
</div>
|
||||
<div class="payment-amount-section">
|
||||
<div class="icon"><img :src="paymentIcon" /></div>
|
||||
<div class="amount-due">{{ this.afterpayPrice }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="payment-card">
|
||||
<div class="calendar-section">
|
||||
<div class="icon"><img :src="calendarIcon" /></div>
|
||||
<div>6 weeks</div>
|
||||
</div>
|
||||
<div class="payment-amount-section">
|
||||
<div class="icon"><img :src="paymentIcon" /></div>
|
||||
<div class="amount-due">{{ this.afterpayPrice }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<div class="legend-item">
|
||||
<img :src="calendarKeyIcon" />
|
||||
<span>Payment Schedule</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<img :src="paymentKeyIcon" />
|
||||
<span>Amount Due</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: "afterpay-breakout",
|
||||
|
||||
props: {
|
||||
cmsWidgetName: String,
|
||||
totalAmount: Number,
|
||||
},
|
||||
data() {
|
||||
return { isExpanded: false };
|
||||
},
|
||||
methods: {
|
||||
toggleIsExpanded() {
|
||||
this.isExpanded = !this.isExpanded;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
afterPayImageUrl() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "Image");
|
||||
},
|
||||
hasImage() {
|
||||
return !!this.imageUrl;
|
||||
},
|
||||
afterpayHeaderCopy() {
|
||||
// Text for Afterpay Breakout experiment
|
||||
|
||||
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
|
||||
},
|
||||
afterpayHeaderSubHeaderCopy() {
|
||||
// Text for Afterpay Breakout experiment
|
||||
|
||||
return this.getCmsContent(this.cmsWidgetName, "SubheaderText");
|
||||
},
|
||||
afterpayPrice() {
|
||||
const adjusted = (this.totalAmount / 4).toFixed(2);
|
||||
return `$${adjusted}`;
|
||||
},
|
||||
infoIcon() {
|
||||
return require(`@/assets/img/icons/info-circle.svg`);
|
||||
},
|
||||
calendarIcon() {
|
||||
return require(`@/assets/img/icons/calendar-icon-black.svg`);
|
||||
},
|
||||
paymentIcon() {
|
||||
return require(`@/assets/img/icons/payment-icon-green.svg`);
|
||||
},
|
||||
calendarKeyIcon() {
|
||||
return require(`@/assets/img/icons/calendar-icon-grey.svg`);
|
||||
},
|
||||
paymentKeyIcon() {
|
||||
return require(`@/assets/img/icons/payment-icon-grey.svg`);
|
||||
},
|
||||
paymentPlanToggleHeading() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "BodyText");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.alert {
|
||||
border-radius: 5px;
|
||||
margin-top: 1rem;
|
||||
|
||||
&.alert-info {
|
||||
.alert-heading {
|
||||
color: #000000;
|
||||
font-weight: 600;
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.afterpay-amount {
|
||||
color: #0c7e47;
|
||||
}
|
||||
.alert-sub-heading {
|
||||
color: #4b4c4e;
|
||||
font-weight: 400;
|
||||
}
|
||||
.after-pay {
|
||||
.after-pay-toggle {
|
||||
cursor: pointer;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
transition: all 0.5s ease;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.9' xml:space='preserve'%3e%3cpath d='M8 8.9c-.2 0-.5-.1-.6-.3L.3 1.5C.1 1.4 0 1.1 0 .9 0 .7.1.4.3.3.4.1.7 0 .9 0c.2 0 .5.1.6.3L8 6.7 14.5.2c.1-.1.4-.2.6-.2.2 0 .5.1.6.3s.3.4.3.6c0 .2-.1.5-.3.6L8.6 8.6c-.1.2-.4.3-.6.3z' fill='%231474a2'/%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right center;
|
||||
margin-left: 0.5rem;
|
||||
width: 1rem;
|
||||
height: 9px;
|
||||
display: inline-flex;
|
||||
}
|
||||
&.expanded:after {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
&.expanded + .after-pay-details {
|
||||
max-height: 500px;
|
||||
transition: all 250ms ease-in;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
.after-pay-toggle a {
|
||||
font-weight: 600;
|
||||
color: #0070d1;
|
||||
text-decoration: none;
|
||||
}
|
||||
.after-pay-details {
|
||||
max-height: 0;
|
||||
transition: all 250ms ease-out;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
.payment-section {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: 0rem;
|
||||
}
|
||||
}
|
||||
.payment-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 6px;
|
||||
border: 0.25px #b3b4b5;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.calendar-section {
|
||||
background-color: #fff; /* White background for calendar area */
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
gap: 2px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.payment-amount-section {
|
||||
background-color: #e0f7e9; /* Light green background for payment area */
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.amount-due {
|
||||
color: #00723e; /* Green text */
|
||||
}
|
||||
.legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 1rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.legend-item span {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -117,6 +117,12 @@ function setupMocks() {
|
|||
|
||||
return "false";
|
||||
}),
|
||||
hasSettingEqualTo: jest.fn((settingName) => {
|
||||
if (settingName === experimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
getCmsContent: jest.fn().mockImplementation(() => {
|
||||
return "test text";
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@
|
|||
:isExpandedOnLoad="false"
|
||||
:isMSRFeeApplicable="isMSRFeeApplicable" />
|
||||
|
||||
<afterpayBreakout
|
||||
v-if="isAfterpayBreakoutDisplay"
|
||||
cmsWidgetName="AfterpayBreakoutAlertWidget"
|
||||
:totalAmount="totalAmountDue" />
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
|
|
@ -118,6 +122,7 @@ import alert from "@/ux-components/alert/alert";
|
|||
import textBlock from "@/digital-components/text-block/text-block";
|
||||
import checkboxQuestion from "@/digital-components/checkbox-question/checkbox-question";
|
||||
import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal";
|
||||
import afterpayBreakout from "@/layouts/payment-method/afterpay-breakout/afterpay-breakout";
|
||||
|
||||
// Supporting Items
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
|
|
@ -131,7 +136,6 @@ import {
|
|||
} from "@/helpers/cms-content-helper";
|
||||
import { paymentMethods } from "@/constants/payment-method-constants";
|
||||
import { experimentSettings } from "@/constants/experiments";
|
||||
import experimentMixin from "@/mixins/experiment-mixin.js";
|
||||
import {
|
||||
revalidatePromosAndValidateQueryStringPromo,
|
||||
buildToastMessagesFromRevalidateOrValidatePromoResponse,
|
||||
|
|
@ -805,6 +809,18 @@ export default {
|
|||
lineItemsCloneForWatcher() {
|
||||
return Object.assign({}, this.lineItems);
|
||||
},
|
||||
isRecalPriceRemove() {
|
||||
return (
|
||||
this.getSettingValue(experimentSettings.RECAL_PRICE_REMOVE)?.toLowerCase() ===
|
||||
"true"
|
||||
);
|
||||
},
|
||||
isAfterpayBreakoutDisplay() {
|
||||
return (
|
||||
(!this.isRecalPriceRemove || !this.isRecalibrationOnOrder) &&
|
||||
this.hasSettingEqualTo(experimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY, "true")
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
customCtaCopy(newValue) {
|
||||
|
|
@ -856,6 +872,7 @@ export default {
|
|||
textBlock,
|
||||
checkboxQuestion,
|
||||
contentGroupModal,
|
||||
afterpayBreakout,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -891,7 +891,7 @@ export default {
|
|||
|
||||
&.funnel-header-wrapper,
|
||||
&.save-progress-popup {
|
||||
display: block;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
marginTopSizeOverride="1" />
|
||||
</template>
|
||||
<locationAlerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
|
||||
<datePickerForPricingByDay
|
||||
<datePicker
|
||||
customComponentId="dateQuestion"
|
||||
selectableDatesSetting="custom"
|
||||
ref="datePicker"
|
||||
|
|
@ -77,7 +77,7 @@ import navbar from "@/fmg-components/nav-bar/nav-bar";
|
|||
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
|
||||
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
||||
import { Form, defineRule } from "vee-validate";
|
||||
import datePickerForPricingByDay from "@/experiment-components/date-picker-for-pricing-by-day";
|
||||
import datePicker from "@/digital-components/date-picker/date-picker";
|
||||
import locationAlerts from "@/layouts/schedule/location-alerts/location-alerts";
|
||||
import timeSlotModalQuestion from "./time-slot-modal-question/time-slot-modal-question";
|
||||
import textBlock from "@/digital-components/text-block/text-block";
|
||||
|
|
@ -298,14 +298,13 @@ export default {
|
|||
);
|
||||
|
||||
// While Pricing By Day Experiment is active, using the updated datePicker
|
||||
const datePickerInitialDataPromise =
|
||||
await datePickerForPricingByDay.methods.loadInitialData({
|
||||
// setup config options for date-picker
|
||||
selectableDatesSetting: "custom",
|
||||
initialViewRowsToShow: 5,
|
||||
customSelectableDatesCallback: getAvailableDates,
|
||||
preSelectedDate: preSelectedDate,
|
||||
});
|
||||
const datePickerInitialDataPromise = await datePicker.methods.loadInitialData({
|
||||
// setup config options for date-picker
|
||||
selectableDatesSetting: "custom",
|
||||
initialViewRowsToShow: 5,
|
||||
customSelectableDatesCallback: getAvailableDates,
|
||||
preSelectedDate: preSelectedDate,
|
||||
});
|
||||
|
||||
// Get pricingByDayUpcharge needed for Pricing By Day
|
||||
const pricingByDayUpchargePartPromise = showPricingByDay
|
||||
|
|
@ -735,7 +734,7 @@ export default {
|
|||
funnelSubHeader,
|
||||
Form,
|
||||
loadingModal,
|
||||
datePickerForPricingByDay,
|
||||
datePicker,
|
||||
locationAlerts,
|
||||
timeSlotModalQuestion,
|
||||
textBlock,
|
||||
|
|
|
|||
|
|
@ -110,6 +110,8 @@ export default {
|
|||
return {
|
||||
serviceZipCode: this.getZipFromStore() ?? this.$route.query.zipcode,
|
||||
emailOrSms: this.getEmailOrSmsFromStore(),
|
||||
carId: this.getCarIdfromStore(),
|
||||
isHeavyTruck: this.getIsVehicleHeavyTruckServiceableFromStore(),
|
||||
displayInvalidZipAlert: false,
|
||||
displayNonServiceableZipAlert: false,
|
||||
};
|
||||
|
|
@ -184,6 +186,20 @@ export default {
|
|||
arePagePrerequisitesValid() {
|
||||
return store.getters.damage.isRepair || store.getters.damage.glassToReplace?.length > 0;
|
||||
},
|
||||
getCarIdfromStore() {
|
||||
return store.getters.vehicle.carId;
|
||||
},
|
||||
getIsVehicleHeavyTruckServiceableFromStore() {
|
||||
return store.getters.vehicle.isBigTruck && store.getters.vehicle.canSafeliteService;
|
||||
},
|
||||
async findClosestApplicableShops() {
|
||||
return await this.dispatchStoreActionWithLogging(
|
||||
storeActions.GET_CLOSEST_APPLICABLE_SHOPS,
|
||||
{ zip: this.serviceZipCode, carId: this.carId },
|
||||
"service-zip"
|
||||
);
|
||||
},
|
||||
|
||||
async backButtonAction() {
|
||||
const skipVin = await skipVinLookup();
|
||||
// route to move backwards
|
||||
|
|
@ -252,6 +268,26 @@ export default {
|
|||
return this.$refs.navbar.removeLoader();
|
||||
}
|
||||
this.displayNonServiceableZipAlert = false;
|
||||
if (this.isHeavyTruck) {
|
||||
const closestShops = await this.findClosestApplicableShops(
|
||||
this.serviceZipCode,
|
||||
this.carId
|
||||
);
|
||||
this.displayNoServiceAlert = !closestShops?.data?.providers?.length;
|
||||
if (this.displayNoServiceAlert) {
|
||||
return this.$refs.navbar.removeLoader();
|
||||
}
|
||||
}
|
||||
if (this.isHeavyTruck) {
|
||||
const closestShops = await this.findClosestApplicableShops(
|
||||
this.serviceZipCode,
|
||||
this.carId
|
||||
);
|
||||
this.displayNoServiceAlert = !closestShops?.data?.providers?.length;
|
||||
if (this.displayNoServiceAlert) {
|
||||
return this.$refs.navbar.removeLoader();
|
||||
}
|
||||
}
|
||||
|
||||
const payment = this.$store.getters.payment;
|
||||
const policy = this.$store.getters.policy;
|
||||
|
|
@ -309,6 +345,8 @@ export default {
|
|||
watch: {
|
||||
serviceZipCode() {
|
||||
this.displayNonServiceableZipAlert = false;
|
||||
this.displayNoServiceAlert = false;
|
||||
this.displayNoServiceAlert = false;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -197,6 +197,8 @@ export default {
|
|||
vin: this.getVinFromStore(),
|
||||
serviceZipCode: this.getZipFromStore() ?? this.$route.query.zipcode,
|
||||
emailOrSms: this.getEmailOrSmsFromStore(),
|
||||
carId: this.getCarIdfromStore(),
|
||||
isHeavyTruck: this.getIsVehicleHeavyTruckServiceableFromStore(),
|
||||
isCarIdDifferent: false,
|
||||
customAlertData: {},
|
||||
previouslyEnteredCarId: "",
|
||||
|
|
@ -245,6 +247,16 @@ export default {
|
|||
);
|
||||
}
|
||||
},
|
||||
getIsVehicleHeavyTruckServiceableFromStore() {
|
||||
return store.getters.vehicle.isBigTruck && store.getters.vehicle.canSafeliteService;
|
||||
},
|
||||
async findClosestApplicableShops() {
|
||||
return await this.dispatchStoreActionWithLogging(
|
||||
storeActions.GET_CLOSEST_APPLICABLE_SHOPS,
|
||||
{ zip: this.serviceZipCode, carId: this.carId },
|
||||
"vin-lookup"
|
||||
);
|
||||
},
|
||||
async forwardButtonAction() {
|
||||
this.resetAlerts();
|
||||
|
||||
|
|
@ -328,6 +340,18 @@ export default {
|
|||
},
|
||||
false
|
||||
);
|
||||
if (this.isHeavyTruck) {
|
||||
closestShops = await this.findClosestApplicableShops(
|
||||
this.serviceZipCode,
|
||||
this.carId
|
||||
);
|
||||
this.displayNoServiceAlert = !closestShops?.data?.providers?.length;
|
||||
if (this.displayNoServiceAlert) {
|
||||
return this.$refs.navbar.removeLoader();
|
||||
}
|
||||
}
|
||||
|
||||
// if no value due to field being optional, blank both phone and email address
|
||||
|
||||
// if no value due to field being optional, blank both phone and email address
|
||||
if (!this.emailOrSms) {
|
||||
|
|
@ -392,7 +416,7 @@ export default {
|
|||
false
|
||||
);
|
||||
}
|
||||
|
||||
// if no value due to field being optional, blank both phone and email address
|
||||
if (!this.emailOrSms) {
|
||||
await this.dispatchStoreAction(storeActions.SAVE_PHONE_NUMBER, "", false);
|
||||
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, "", false);
|
||||
|
|
@ -474,6 +498,8 @@ export default {
|
|||
this.displayInvalidZipAlert = false;
|
||||
this.displayVinNotFoundAlert = false;
|
||||
this.displayVinScanFailedAlert = false;
|
||||
this.displayNoServiceAlert = false;
|
||||
this.displayNoServiceAlert = false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue