PageSourceSearch

https://www.gbliners.com/app/plugins/gbliners-forms/src/js/budget.js?ver=c27211

js gbliners.com collected 2026-09-29 08:22:12 UTC 18,652 bytes, 819 lines download raw bytes

1const budget = {
2	api_base: portalFormSettings.base_url,
3	ajax_started: false,
4
5	data: {
6		uuid: window.uuidv4(),
7		user_identifier: null,
8		client_ip: portalFormSettings.client_ip,
9		source: portalFormSettings.source,
10
11		title: null,
12		first_name: null,
13		last_name: null,
14		email: null,
15		telephone: null,
16		telephone_secondary: null,
17		best_time_to_contact: null,
18
19		from: {
20			loqate_id: null,
21			address_1: null,
22			address_2: null,
23			locality: null,
24			state: null,
25			postcode: null,
26			country: 'gb',
27		},
28
29		to: {
30			loqate_id: null,
31			address_1: null,
32			address_2: null,
33			locality: null,
34			state: null,
35			postcode: null,
36			country: 'gb',
37		},
38
39		to_unknown: false,
40		to_approx_location: null,
41		estimated_removal_date: null,
42		estimated_removal_date_confirmed: null,
43		comments: null,
44
45		from_property: {
46			bedrooms: null,
47			is_flat: null,
48			floor: null,
49			has_lift: null,
50		},
51
52		to_property: {
53			bedrooms: null,
54			is_flat: null,
55			floor: null,
56			has_lift: null,
57		},
58
59		services: {
60			packing: null,
61			storage: null,
62			storage_weeks: null,
63		},
64
65		quote_method: null,
66		slot_date: null,
67		slot_time: null,
68		slot_id: null,
69		reservation_id: null,
70
71		video_attendee_name: null,
72		video_attendee_contact_no: null,
73	},
74	step: 1,
75	steps: 4,
76	step_is_valid: true,
77	budget: null,
78	distance: null,
79	slot_picker_el: null,
80	slots_loaded: false,
81	slot_date_invalid: false,
82	slots: [],
83	time_slots: [],
84	reserved_slot: {},
85	error: null,
86
87	init() {
88		// this.setTestData();
89
90		window.dataLayer = window.dataLayer || [];
91
92		// State management
93		window.history.replaceState(
94			{ step: this.step },
95			'',
96			'#step' + this.step
97		);
98		window.addEventListener('popstate', (event) => {
99			if (event.state.step) {
100				this.step = event.state.step;
101			}
102		});
103
104		this.$refs.step1.addEventListener('submit', function (e) {
105			e.preventDefault();
106		});
107
108		this.$refs.step2.addEventListener('submit', function (e) {
109			e.preventDefault();
110		});
111
112		this.$refs.step3.addEventListener('submit', function (e) {
113			e.preventDefault();
114		});
115
116		this.$refs.step4.addEventListener('submit', function (e) {
117			e.preventDefault();
118		});
119
120		window.addEventListener('budgetFormStepComplete', function (event) {
121			window.dataLayer.push({
122				event: 'formSubmission',
123				formProgress: 'form_step_' + event.detail.step + '_completed',
124				formName: 'cost_calculator',
125			});
126		});
127
128		window.addEventListener('budgetFormSubmitted', function () {
129			window.dataLayer.push({
130				event: 'formSubmission',
131				formProgress: 'form_completed',
132				formName: 'cost_calculator',
133			});
134		});
135
136		this.$nextTick(() => {
137			this.initLoqate();
138		});
139
140		// Clear any error classes after change of state
141		const inputs = document.querySelectorAll('input, select, textarea');
142		inputs.forEach((input) => {
143			input.addEventListener('change', () => {
144				const inputsToClear = document.querySelectorAll(
145					'input, select, textarea'
146				);
147				inputsToClear.forEach((inputToClear) => {
148					inputToClear.classList.remove('tw-ring-2');
149					inputToClear.classList.remove('tw-ring-red-400');
150					inputToClear.setCustomValidity('');
151				});
152				this.error = '';
153			});
154		});
155	},
156
157	initLoqate() {
158		const options = {
159			key: portalFormSettings.loqate_api_key,
160			countries: {
161				codesList: 'GB',
162			},
163			setCountryByIp: false
164		};
165
166		const fromFields = [
167			{
168				element: 'from.address_1',
169				field: 'Line1',
170				mode: pca.fieldMode.POPULATE | pca.fieldMode.SEARCH,
171			},
172			{
173				element: 'from.address_2',
174				field: 'Line2',
175				mode: pca.fieldMode.POPULATE,
176			},
177			{
178				element: 'from.locality',
179				field: 'City',
180				mode: pca.fieldMode.POPULATE,
181			},
182			{
183				element: 'from.state',
184				field: 'Province',
185				mode: pca.fieldMode.POPULATE,
186			},
187			{
188				element: 'from.postcode',
189				field: 'PostalCode',
190				mode: pca.fieldMode.POPULATE,
191			},
192			{
193				element: 'from.country',
194				field: 'CountryCode',
195				mode: pca.fieldMode.COUNTRY | pca.fieldMode.POPULATE,
196			},
197		];
198
199		const toFields = [
200			{
201				element: 'to.address_1',
202				field: 'Line1',
203				mode: pca.fieldMode.POPULATE | pca.fieldMode.SEARCH,
204			},
205			{
206				element: 'to.address_2',
207				field: 'Line2',
208				mode: pca.fieldMode.POPULATE,
209			},
210			{
211				element: 'to.locality',
212				field: 'City',
213				mode: pca.fieldMode.POPULATE,
214			},
215			{
216				element: 'to.state',
217				field: 'Province',
218				mode: pca.fieldMode.POPULATE,
219			},
220			{
221				element: 'to.postcode',
222				field: 'PostalCode',
223				mode: pca.fieldMode.POPULATE,
224			},
225			{
226				element: 'to.country',
227				field: 'CountryCode',
228				mode: pca.fieldMode.COUNTRY | pca.fieldMode.POPULATE,
229			},
230		];
231
232		const fromControl = new pca.Address(fromFields, options);
233		const toControl = new pca.Address(toFields, options);
234
235		fromFields.forEach((field) => {
236			const fieldElement = document.getElementById(field.element);
237			if (!fieldElement) {
238				return;
239			}
240
241			fieldElement.addEventListener(
242				'change',
243				() => (this.data.from.loqate_id = null)
244			);
245		});
246
247		toFields.forEach((field) => {
248			const fieldElement = document.getElementById(field.element);
249			if (!fieldElement) {
250				return;
251			}
252
253			fieldElement.addEventListener(
254				'change',
255				() => (this.data.to.loqate_id = null)
256			);
257		});
258
259		fromControl.listen('populate', (address) => {
260			this.populateAddress(this.data.from, address);
261		});
262
263		toControl.listen('populate', (address) => {
264			this.populateAddress(this.data.to, address);
265		});
266	},
267
268	populateAddress(address, loqateData) {
269		let line1 = loqateData.Line1;
270		let line2 = loqateData.Line2;
271		if (loqateData.Line3 || loqateData.Line4 || loqateData.Line5) {
272			line1 += ', ' + loqateData.Line2;
273			line2 = [loqateData.Line3, loqateData.Line4, loqateData.Line5]
274				.filter((value) => value)
275				.join(', ');
276		}
277
278		address.loqate_id = loqateData.Id;
279		address.address_1 = line1;
280		address.address_2 = line2;
281		address.locality = loqateData.City;
282		address.state = loqateData.Province;
283		address.postcode = loqateData.PostalCode;
284	},
285
286	getStepForm($refs) {
287		if (!$refs) {
288			$refs = this.$refs;
289		}
290
291		/**
292		 * @member FormElement form
293		 */
294		let form;
295		switch (this.step) {
296			case 1:
297				form = $refs.step1;
298				break;
299			case 2:
300				form = $refs.step2;
301				break;
302			case 3:
303				form = $refs.step3;
304				break;
305			case 4:
306				form = $refs.step4;
307				break;
308		}
309
310		return form;
311	},
312
313	async prevStep() {
314		if (this.ajax_started === true) {
315			return;
316		}
317
318		this.error = null;
319		this.step = Math.max(this.step - 1, 1);
320		this.validateStep(this.$refs, true);
321
322		await this.$nextTick;
323
324		this.getStepForm().scrollIntoView({
325			behavior: 'smooth',
326			inline: 'start',
327		});
328	},
329
330	async nextStep() {
331		if (this.ajax_started === true) {
332			return;
333		}
334
335		this.error = null;
336		this.validateStep(this.$refs, true);
337
338		if (!this.step_is_valid) {
339			return;
340		}
341
342		const event = new CustomEvent('budgetFormStepComplete', {
343			detail: {
344				step: this.step,
345			},
346		});
347		window.dispatchEvent(event);
348
349		this.step = Math.min(this.step + 1, this.steps);
350		window.history.pushState({ step: this.step }, '', '#step' + this.step);
351
352		if (this.step === 3) {
353			await this.getQuote();
354		}
355
356		if (this.step === 4) {
357			await this.submitLead();
358		}
359
360		await this.$nextTick;
361
362		this.getStepForm().scrollIntoView({
363			behavior: 'smooth',
364			inline: 'start',
365		});
366	},
367
368	setUserIdentifier(identifier) {
369		this.data.user_identifier = identifier;
370	},
371
372	buildBudgetRequestData() {
373		const data = this.data;
374
375		data.form = 'ONLINE_BUDGET_FORM';
376		data.state = 'DRAFT';
377
378		return data;
379	},
380
381	buildLeadRequestData() {
382		const data = this.data;
383
384		data.form = 'ONLINE_BUDGET_FORM';
385		data.state = 'COMPLETE';
386
387		return data;
388	},
389
390	setTestData() {
391		this.data.title = 'Mr';
392		this.data.first_name = 'Matt';
393		this.data.last_name = 'Jones';
394		this.data.email = '[email protected]';
395		this.data.telephone = '01568612460';
396		this.data.telephone_secondary = '07815192734';
397		this.data.best_time_to_contact = 'Evening';
398
399		this.data.from.address_1 = '3 Arrow Close';
400		this.data.from.address_2 = 'Leominster Enterprise Park';
401		this.data.from.locality = 'Leominster';
402		this.data.from.state = 'Herefordshire';
403		this.data.from.postcode = 'HR6 0LD';
404		this.data.from.country = 'gb';
405
406		this.data.to.address_1 = 'Skylon Court';
407		this.data.to.address_2 = 'Coldnose Road';
408		this.data.to.locality = 'Hereford';
409		this.data.to.state = 'Herefordshire';
410		this.data.to.postcode = 'HR2 6JS';
411		this.data.to.country = 'gb';
412
413		this.data.to_unknown = false;
414		this.data.estimated_removal_date = '2022-06-01';
415		this.data.estimated_removal_date_confirmed = true;
416		this.data.comments = 'Please be careful with the grand piano.';
417
418		this.data.from_property.bedrooms = '5';
419		this.data.from_property.is_flat = false;
420		this.data.from_property.floor = null;
421		this.data.from_property.has_lift = null;
422
423		this.data.to_property.bedrooms = '3';
424		this.data.to_property.is_flat = true;
425		this.data.to_property.floor = 'ground';
426		this.data.to_property.has_lift = true;
427
428		this.data.quote_method = 'video';
429
430		this.data.services.packing = true;
431		this.data.services.storage = true;
432		this.data.services.storage_weeks = 12;
433
434		this.data.video_attendee_name = 'Matt Jones';
435		this.data.video_attendee_contact_no = '01568612460';
436	},
437
438	async handleRemoteErrors(error, requestType) {
439		// Something went wrong with getting the budget, put back to step 3
440		this.errors = error.response.data.errors;
441
442		if (error.response.status === 422) {
443			this.step = this.getErrorStep(error.response.data.errors);
444			await this.$nextTick();
445			this.addCustomErrors(error.response.data.errors);
446			this.validateStep(this.$refs, true);
447			this.error =
448				'Some fields failed validation, please check for any errors above and contact us if you continue to have difficulty';
449			return;
450		}
451
452		if (requestType === 'getAvailableSlots') {
453			this.step = 3;
454			this.addCustomErrors({
455				quote_method: [
456					"Unable to load calendar for video slots. Please select 'callback' for help from our team.",
457				],
458			});
459			this.validateStep(this.$refs, true);
460			this.error =
461				"Unable to load calendar for video slots. Please select 'callback' for help from our team.";
462			return;
463		}
464
465		if (requestType === 'submitLead') {
466			this.step = 3;
467			this.validateStep(this.$refs, true);
468			this.error = 'Unable to submit form. Please contact us.';
469			return;
470		}
471
472		if (error.response.status === 404) {
473			switch (requestType) {
474				case 'reserveSlot':
475					this.addCustomErrors({
476						slot_time: [
477							'Slot no longer available, please choose another.',
478						],
479					});
480					this.validateStep(this.$refs, true);
481					this.error =
482						'Unable to reserve video slot. Please choose another.';
483					break;
484				case 'cancelSlotReservation':
485					this.validateStep(this.$refs, true);
486					this.error =
487						'Unable to cancel video survey reservation. Please contact us.';
488					break;
489			}
490		}
491	},
492
493	initDatePicker($el) {
494		const currentDate = new Date();
495		let bankHolidays = [];
496
497		axios
498			.get(`https://www.gov.uk/bank-holidays.json`, {
499				headers: {
500					Accept: 'application/json',
501				},
502			})
503			.then((response) => {
504				bankHolidays = response.data['england-and-wales'].events.map(
505					(item) => item.date
506				);
507			})
508			.finally(() => {
509				new easepick.create({
510					element: $el,
511					css: [
512						'https://cdn.jsdelivr.net/npm/@easepick/[email protected]/dist/index.css',
513						'https://cdn.jsdelivr.net/npm/@easepick/[email protected]/dist/index.css',
514						portalFormSettings.css_path + 'easepick.css',
515					],
516					zIndex: 10,
517					inline: true,
518					readonly: false,
519					plugins: [LockPlugin],
520					LockPlugin: {
521						minDate: currentDate.setDate(currentDate.getDate() + 1),
522						filter(date) {
523							if (date.getDay() === 0) {
524								return true;
525							}
526
527							if (
528								bankHolidays.includes(
529									date.format('YYYY-MM-DD')
530								)
531							) {
532								return true;
533							}
534						},
535					},
536					setup: (picker) => {
537						picker.on('select', (e) => {
538							const { date } = e.detail;
539							this.data.estimated_removal_date =
540								date.format('YYYY-MM-DD');
541						});
542					},
543				});
544			});
545	},
546
547	initSlotPicker($el) {
548		this.slot_picker_el = $el;
549
550		this.getAvailableSlots();
551
552		setInterval(this.getAvailableSlots, 1000 * 60 * 30);
553	},
554
555	async getQuote() {
556		this.ajax_started = true;
557		return axios
558			.post(`${this.api_base}budget`, this.buildBudgetRequestData(), {
559				headers: {
560					'Content-Type': 'application/json',
561					Accept: 'application/json',
562				},
563			})
564			.then((response) => {
565				this.budget = new Intl.NumberFormat('en-GB').format(
566					response.data.budget
567				);
568				this.distance = response.data.distance;
569				const event = new CustomEvent('budgetShown', {
570					detail: { budget: this.budget },
571				});
572				window.dispatchEvent(event);
573				this.ajax_started = false;
574			})
575			.catch(async (error) => {
576				await this.handleRemoteErrors(error, 'getQuote');
577				this.ajax_started = false;
578			});
579	},
580
581	getAvailableSlots() {
582		this.slots_loaded = false;
583		this.time_slots = [];
584		this.ajax_started = true;
585
586		axios
587			.get(`${this.api_base}surveys/slots`, {
588				headers: {
589					Accept: 'application/json',
590				},
591			})
592			.then((response) => this.processAvailableSlots(response))
593			.catch(async (error) => {
594				this.ajax_started = false;
595				await this.handleRemoteErrors(error, 'getAvailableSlots');
596			});
597	},
598
599	processAvailableSlots(response) {
600		const currentDate = new Date();
601		this.slots = response.data;
602		this.slots_loaded = true;
603		this.ajax_started = false;
604
605		const allowedDates = this.slots.map((slot) => {
606			return slot.date;
607		});
608
609		new easepick.create({
610			element: this.slot_picker_el,
611			css: [
612				'https://cdn.jsdelivr.net/npm/@easepick/[email protected]/dist/index.css',
613				'https://cdn.jsdelivr.net/npm/@easepick/[email protected]/dist/index.css',
614				portalFormSettings.css_path + 'easepick.css',
615			],
616			zIndex: 10,
617			inline: true,
618			readonly: false,
619			plugins: [LockPlugin],
620			LockPlugin: {
621				minDate: currentDate.setDate(currentDate.getDate() + 1),
622				filter(date) {
623					return !allowedDates.includes(date.format('YYYY-MM-DD'));
624				},
625			},
626			setup: (picker) => {
627				picker.on('preselect', () => {
628					this.time_slots = [];
629				});
630				picker.on('select', async (e) => {
631					const { date } = e.detail;
632					this.data.slot_date = date.format('YYYY-MM-DD');
633					this.data.slot_time = null;
634
635					await this.cancelSlotReservation();
636
637					this.time_slots = this.slots
638						.filter((slot) => {
639							return slot.date === this.data.slot_date;
640						})
641						.map((slot) => {
642							return slot.startTime;
643						});
644				});
645			},
646		});
647	},
648
649	async changeReservedSlot($el) {
650		$el.classList.remove('tw-ring-2');
651		$el.classList.remove('tw-ring-red-400');
652		$el.setCustomValidity('');
653		this.error = '';
654
655		if (this.data.reservation_id) {
656			await this.cancelSlotReservation();
657		}
658
659		if (this.data.slot_time && this.data.slot_time !== 'null') {
660			await this.reserveSlot();
661		}
662	},
663
664	async cancelSlotReservation() {
665		if (!this.data.reservation_id) {
666			this.reserved_slot = null;
667			return;
668		}
669
670		this.ajax_started = true;
671
672		return axios
673			.post(
674				`${this.api_base}surveys/cancel`,
675				{
676					reservation_id: this.data.reservation_id,
677				},
678				{
679					headers: {
680						Accept: 'application/json',
681					},
682				}
683			)
684			.then(() => {
685				this.reserved_slot = null;
686				this.data.reservation_id = null;
687				this.ajax_started = false;
688			})
689			.catch(async () => {
690				// await this.handleRemoteErrors(error, 'cancelSlotReservation');
691				this.ajax_started = false;
692			});
693	},
694
695	async reserveSlot() {
696		this.ajax_started = true;
697
698		return axios
699			.post(
700				`${this.api_base}surveys/reserve`,
701				{
702					date: this.data.slot_date,
703					time: this.data.slot_time,
704				},
705				{
706					headers: {
707						Accept: 'application/json',
708					},
709				}
710			)
711			.then((response) => {
712				this.reserved_slot = response.data;
713				this.data.slot_id = response.data.slotId;
714				this.data.reservation_id = response.data.reservationId;
715				this.ajax_started = false;
716			})
717			.catch(async (error) => {
718				await this.handleRemoteErrors(error, 'reserveSlot');
719				this.ajax_started = false;
720			});
721	},
722
723	submitLead() {
724		this.ajax_started = true;
725		return axios
726			.post(`${this.api_base}lead/move`, this.buildLeadRequestData(), {
727				headers: {
728					Accept: 'application/json',
729				},
730			})
731			.then(() => {
732				this.ajax_started = false;
733				const event = new CustomEvent('budgetFormSubmitted');
734				window.dispatchEvent(event);
735			})
736			.catch(async (error) => {
737				await this.handleRemoteErrors(error, 'submitLead');
738				this.ajax_started = false;
739			});
740	},
741
742	validateStep($refs, showErrors = false) {
743		const form = this.getStepForm($refs);
744		this.step_is_valid = form.checkValidity();
745		this.slot_date_invalid = false;
746
747		if (showErrors) {
748			form.reportValidity();
749
750			// If showing errors, then highlight error fields
751			const inputs = form.querySelectorAll('input, select, textarea');
752			inputs.forEach((input) => {
753				if (input.name === 'slot_date') {
754					this.slot_date_invalid = true;
755				}
756				if (!input.checkValidity()) {
757					input.classList.add('tw-ring-2');
758					input.classList.add('tw-ring-red-400');
759				} else {
760					input.classList.remove('tw-ring-2');
761					input.classList.remove('tw-ring-red-400');
762				}
763			});
764		}
765	},
766
767	timeToDisplayTime(time) {
768		const utcDate = moment.tz(
769			this.data.slot_date + 'T' + time,
770			'Europe/London'
771		);
772		return utcDate.format('dddd, MMMM Do YYYY - h:mm a') + ' (UK Time)';
773	},
774
775	getErrorStep(errors) {
776		if (typeof errors !== 'undefined') {
777			const firstError = Object.keys(errors)[0];
778			const fieldName = firstError.replace('.', '\\.');
779			const field = document.querySelector('[name=' + fieldName + ']');
780			if (field) {
781				const step = field.closest('form');
782				if (step) {
783					return parseInt(step.dataset.step);
784				}
785			}
786		}
787		return Math.max(this.step - 1, 1);
788	},
789
790	addCustomErrors(errors) {
791		if (typeof errors !== 'undefined') {
792			Object.keys(errors).forEach((field) => {
793				const fieldName = field.replace('.', '\\.');
794				const fieldElement = document.querySelector(
795					'[name=' + fieldName + ']'
796				);
797				if (fieldElement && field) {
798					fieldElement.setCustomValidity(errors[field][0]);
799				}
800			});
801		}
802	},
803
804	amendQuote() {
805		this.data.uuid = window.uuidv4();
806		this.step = 1;
807	},
808
809	formatAddress(address, lookForUnknown = false) {
810		if (lookForUnknown && this.data.to_unknown) {
811			return this.data.to_approx_location;
812		}
813		return `${address.address_1}, ${
814			address.address_2 ? `${address.address_2},` : ''
815		} ${address.locality}, ${address.state ? `${address.state},` : ''} ${
816			address.postcode
817		}`;
818	},
819};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.