abmp-npm 2.0.76 → 2.0.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/package.json +8 -1
  2. package/pages/Home.js +27 -55
  3. package/pages/index.js +0 -1
  4. package/public/Utils/personalDetailsUtils.js +5 -1
  5. package/public/Utils/sharedUtils.js +32 -0
  6. package/public/consts.js +0 -1
  7. package/.claude/worktrees/fix-save-button-disable/.husky/pre-commit +0 -19
  8. package/.claude/worktrees/fix-save-button-disable/.prettierignore +0 -7
  9. package/.claude/worktrees/fix-save-button-disable/.prettierrc.json +0 -16
  10. package/.claude/worktrees/fix-save-button-disable/README.md +0 -1
  11. package/.claude/worktrees/fix-save-button-disable/backend/README-member-contact-flows.md +0 -136
  12. package/.claude/worktrees/fix-save-button-disable/backend/__tests__/daily-pull-execution-check.test.js +0 -124
  13. package/.claude/worktrees/fix-save-button-disable/backend/__tests__/ensure-unique-urls-in-batch.test.js +0 -129
  14. package/.claude/worktrees/fix-save-button-disable/backend/__tests__/login-email-sync.test.js +0 -49
  15. package/.claude/worktrees/fix-save-button-disable/backend/__tests__/transient-retry-and-bulk-lookup.test.js +0 -129
  16. package/.claude/worktrees/fix-save-button-disable/backend/__tests__/url-uniqueness.test.js +0 -194
  17. package/.claude/worktrees/fix-save-button-disable/backend/automations-methods.js +0 -13
  18. package/.claude/worktrees/fix-save-button-disable/backend/cms-data-methods.js +0 -251
  19. package/.claude/worktrees/fix-save-button-disable/backend/consts.js +0 -58
  20. package/.claude/worktrees/fix-save-button-disable/backend/contacts-methods.js +0 -67
  21. package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/bulk-process-methods.js +0 -222
  22. package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/consts.js +0 -39
  23. package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/index.js +0 -5
  24. package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/process-member-methods.js +0 -213
  25. package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/schedule-methods.js +0 -41
  26. package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/sync-to-cms-methods.js +0 -161
  27. package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/utils.js +0 -151
  28. package/.claude/worktrees/fix-save-button-disable/backend/data-hooks.js +0 -29
  29. package/.claude/worktrees/fix-save-button-disable/backend/dev-only-methods.js +0 -58
  30. package/.claude/worktrees/fix-save-button-disable/backend/elevated-modules.js +0 -18
  31. package/.claude/worktrees/fix-save-button-disable/backend/forms-methods.js +0 -52
  32. package/.claude/worktrees/fix-save-button-disable/backend/http-functions/httpFunctions.js +0 -86
  33. package/.claude/worktrees/fix-save-button-disable/backend/http-functions/index.js +0 -3
  34. package/.claude/worktrees/fix-save-button-disable/backend/http-functions/interests.js +0 -35
  35. package/.claude/worktrees/fix-save-button-disable/backend/index.js +0 -19
  36. package/.claude/worktrees/fix-save-button-disable/backend/jobs.js +0 -158
  37. package/.claude/worktrees/fix-save-button-disable/backend/login/generate-member-session-token.js +0 -27
  38. package/.claude/worktrees/fix-save-button-disable/backend/login/index.js +0 -8
  39. package/.claude/worktrees/fix-save-button-disable/backend/login/qa-login-methods.js +0 -54
  40. package/.claude/worktrees/fix-save-button-disable/backend/login/sso-methods.js +0 -159
  41. package/.claude/worktrees/fix-save-button-disable/backend/member-contact-orchestration.js +0 -111
  42. package/.claude/worktrees/fix-save-button-disable/backend/members-area-methods.js +0 -88
  43. package/.claude/worktrees/fix-save-button-disable/backend/members-data-methods.js +0 -789
  44. package/.claude/worktrees/fix-save-button-disable/backend/pac-api-methods.js +0 -54
  45. package/.claude/worktrees/fix-save-button-disable/backend/routers/index.js +0 -3
  46. package/.claude/worktrees/fix-save-button-disable/backend/routers/methods.js +0 -191
  47. package/.claude/worktrees/fix-save-button-disable/backend/routers/utils.js +0 -128
  48. package/.claude/worktrees/fix-save-button-disable/backend/search-filters-methods.js +0 -124
  49. package/.claude/worktrees/fix-save-button-disable/backend/tasks/address-primary-methods.js +0 -256
  50. package/.claude/worktrees/fix-save-button-disable/backend/tasks/address-visibility-methods.js +0 -160
  51. package/.claude/worktrees/fix-save-button-disable/backend/tasks/consts.js +0 -34
  52. package/.claude/worktrees/fix-save-button-disable/backend/tasks/daily-pull-check-methods.js +0 -78
  53. package/.claude/worktrees/fix-save-button-disable/backend/tasks/email-normalize-methods.js +0 -134
  54. package/.claude/worktrees/fix-save-button-disable/backend/tasks/index.js +0 -10
  55. package/.claude/worktrees/fix-save-button-disable/backend/tasks/migration-methods.js +0 -46
  56. package/.claude/worktrees/fix-save-button-disable/backend/tasks/tasks-configs.js +0 -251
  57. package/.claude/worktrees/fix-save-button-disable/backend/tasks/tasks-helpers-methods.js +0 -433
  58. package/.claude/worktrees/fix-save-button-disable/backend/tasks/tasks-process-methods.js +0 -606
  59. package/.claude/worktrees/fix-save-button-disable/backend/tasks/url-migration-methods.js +0 -378
  60. package/.claude/worktrees/fix-save-button-disable/backend/tasks/url-space-fix-methods.js +0 -174
  61. package/.claude/worktrees/fix-save-button-disable/backend/utils.js +0 -277
  62. package/.claude/worktrees/fix-save-button-disable/dev-only-scripts/extract-duplicate-url-groups.js +0 -201
  63. package/.claude/worktrees/fix-save-button-disable/dev-only-scripts/find-duplicate-ids.js +0 -159
  64. package/.claude/worktrees/fix-save-button-disable/dev-only-scripts/find-duplicate-urls.js +0 -201
  65. package/.claude/worktrees/fix-save-button-disable/eslint.config.js +0 -120
  66. package/.claude/worktrees/fix-save-button-disable/index.js +0 -5
  67. package/.claude/worktrees/fix-save-button-disable/package-lock.json +0 -12160
  68. package/.claude/worktrees/fix-save-button-disable/package.json +0 -60
  69. package/.claude/worktrees/fix-save-button-disable/pages/ContactUs.js +0 -129
  70. package/.claude/worktrees/fix-save-button-disable/pages/Home.js +0 -779
  71. package/.claude/worktrees/fix-save-button-disable/pages/LearnMore.js +0 -27
  72. package/.claude/worktrees/fix-save-button-disable/pages/LoadingPage.js +0 -29
  73. package/.claude/worktrees/fix-save-button-disable/pages/Profile.js +0 -354
  74. package/.claude/worktrees/fix-save-button-disable/pages/QAPage.js +0 -39
  75. package/.claude/worktrees/fix-save-button-disable/pages/SaveAlerts.js +0 -13
  76. package/.claude/worktrees/fix-save-button-disable/pages/SelectBannerImages.js +0 -46
  77. package/.claude/worktrees/fix-save-button-disable/pages/deleteConfirm.js +0 -19
  78. package/.claude/worktrees/fix-save-button-disable/pages/index.js +0 -12
  79. package/.claude/worktrees/fix-save-button-disable/pages/personalDetails.js +0 -2486
  80. package/.claude/worktrees/fix-save-button-disable/public/Utils/homePage.js +0 -798
  81. package/.claude/worktrees/fix-save-button-disable/public/Utils/personalDetailsUtils.js +0 -25
  82. package/.claude/worktrees/fix-save-button-disable/public/Utils/sharedUtils.js +0 -235
  83. package/.claude/worktrees/fix-save-button-disable/public/consts.js +0 -113
  84. package/.claude/worktrees/fix-save-button-disable/public/index.js +0 -5
  85. package/.claude/worktrees/fix-save-button-disable/public/messages.js +0 -16
  86. package/.claude/worktrees/fix-save-button-disable/public/sso-auth-methods.js +0 -43
  87. package/.husky/pre-commit +0 -19
  88. package/.prettierignore +0 -7
  89. package/.prettierrc.json +0 -16
  90. package/backend/__tests__/daily-pull-execution-check.test.js +0 -124
  91. package/backend/__tests__/ensure-unique-urls-in-batch.test.js +0 -129
  92. package/backend/__tests__/login-email-sync.test.js +0 -49
  93. package/backend/__tests__/transient-retry-and-bulk-lookup.test.js +0 -129
  94. package/backend/__tests__/url-uniqueness.test.js +0 -194
  95. package/dev-only-scripts/extract-duplicate-url-groups.js +0 -201
  96. package/dev-only-scripts/find-duplicate-ids.js +0 -159
  97. package/dev-only-scripts/find-duplicate-urls.js +0 -201
  98. package/eslint.config.js +0 -120
  99. package/pages/contactForLocation.js +0 -28
@@ -1,2486 +0,0 @@
1
- const { location: wixLocation } = require('@wix/site-location');
2
- const { window: wixWindow } = require('@wix/site-window');
3
- const _ = require('lodash');
4
-
5
- const {
6
- ADDRESS_STATUS_TYPES,
7
- DEFAULT_BUSINESS_NAME_TEXT,
8
- FREE_WEBSITE_TEXT_STATES,
9
- LIGHTBOX_NAMES,
10
- } = require('../public/consts');
11
- const { handleOnCustomValidation, isNotValidUrl } = require('../public/Utils/personalDetailsUtils');
12
- const {
13
- generateId,
14
- isWixHostedImage,
15
- normalizeExternalUrl,
16
- } = require('../public/Utils/sharedUtils');
17
-
18
- const MAX_PHONES_COUNT = 10;
19
- const MAX_ADDRESSES_COUNT = 10;
20
-
21
- /** US 10-digit NANP: strip non-digits; drop leading 1 from 11-digit input. */
22
- function digitsFromPhoneInput(value) {
23
- if (!value || typeof value !== 'string') return '';
24
- let d = value.replace(/\D/g, '');
25
- if (d.length === 11 && d[0] === '1') d = d.slice(1);
26
- return d.slice(0, 10);
27
- }
28
-
29
- /** Format as (###) ###-#### while typing. */
30
- function formatUsPhoneInput(digits) {
31
- if (!digits) return '';
32
- const d = digits.slice(0, 10);
33
- if (d.length <= 3) return `(${d}`;
34
- if (d.length <= 6) return `(${d.slice(0, 3)}) ${d.slice(3)}`;
35
- return `(${d.slice(0, 3)}) ${d.slice(3, 6)}-${d.slice(6)}`;
36
- }
37
-
38
- function formatPhoneNumberForInput(value) {
39
- return formatUsPhoneInput(digitsFromPhoneInput(value || ''));
40
- }
41
-
42
- function phoneDigitsEqual(a, b) {
43
- const da = digitsFromPhoneInput(String(a || ''));
44
- const db = digitsFromPhoneInput(String(b || ''));
45
- return da.length > 0 && db.length > 0 && da === db;
46
- }
47
-
48
- const ADDRESS_STATES = {
49
- VIEW: 'addressViewState',
50
- EDIT: 'addressEditState',
51
- };
52
-
53
- const TESTIMONIAL_STATES = {
54
- VIEW: 'testimonialState',
55
- ADD: 'addTestimonialState',
56
- };
57
-
58
- const GALLERY_STATES = {
59
- VIEW: 'imageState',
60
- ADD: 'addImageState',
61
- };
62
-
63
- const MAIN_STATE_BOX_STATES = {
64
- FORM_STATE: 'formState',
65
- UNAUTHORIZED_STATE: 'unauthorizedState',
66
- ERROR_STATE: 'errorState',
67
- LOADING_STATE: 'loading',
68
- };
69
-
70
- const FALLBACK_ADDRESS_STATUS = ADDRESS_STATUS_TYPES.STATE_CITY_ZIP;
71
-
72
- const PAGE_NAME = 'Personal Details';
73
- const BUTTON_NAMES = {
74
- PERSONAL: 'Save Personal Details',
75
- BUSINESS: 'Save Business & Services',
76
- CONTACT: 'Save Contact & Booking',
77
- };
78
-
79
- const FORM_SECTION_HANDLER_MAP = {
80
- PERSONAL: { section: 'personal', handler: null }, // handler will be set in init
81
- BUSINESS_SERVICES: { section: 'businessServices', handler: null },
82
- CONTACT_BOOKING: { section: 'contactBooking', handler: null },
83
- DIRECTORY_OPT_OUT: { section: 'directoryOptOut', handler: null },
84
- WEBSITE_OPT_OUT: { section: 'websiteOptOut', handler: null },
85
- };
86
- const ContactEmailValidity = {
87
- valid: true,
88
- validationMessage: '',
89
- };
90
- const CONTACT_EMAIL_VALIDATION_MESSAGES = {
91
- REQUIRED: 'Email is required',
92
- ALREADY_TAKEN: 'Email is already taken. Please choose a different one.',
93
- ERROR: 'There was an error. Please try again.',
94
- };
95
-
96
- async function personalDetailsOnReady({
97
- $w: _$w,
98
- getInterestAll,
99
- saveRegistrationData,
100
- validateMemberToken,
101
- checkUrlUniqueness,
102
- isEmailAlreadyUsed,
103
- trackClick,
104
- }) {
105
- let itemMemberObj = {};
106
- let originalMemberData = {};
107
- let selectedServices = [];
108
- const uploadedImages = {
109
- profileImage: '',
110
- logoImage: '',
111
- bannerImage: '',
112
- };
113
-
114
- const formHasUnsavedChanges = {
115
- [FORM_SECTION_HANDLER_MAP.PERSONAL.section]: false,
116
- [FORM_SECTION_HANDLER_MAP.BUSINESS_SERVICES.section]: false,
117
- [FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING.section]: false,
118
- [FORM_SECTION_HANDLER_MAP.DIRECTORY_OPT_OUT.section]: false,
119
- [FORM_SECTION_HANDLER_MAP.WEBSITE_OPT_OUT.section]: false,
120
- };
121
-
122
- const slugValidationTimeout = {};
123
- const contactEmailValidationTimeout = {};
124
- let isSlugValid = true;
125
- let currentSlugValidationId = 0;
126
- let currentContactEmailValidationId = 0;
127
-
128
- const SLUG_FLAGS = {
129
- VALID: '#validSlugFlag',
130
- INVALID: '#invalidSlugFlag',
131
- };
132
-
133
- const SLUG_MESSAGES = {
134
- INVALID_FORMAT: 'Enter a valid URL. You can use letters, numbers or dashes.',
135
- TAKEN: 'Enter a new URL slug. This one is already taken.',
136
- ERROR: 'There was an error. Please try again.',
137
- };
138
-
139
- _$w('#mainMultiStateBox').changeState(MAIN_STATE_BOX_STATES.LOADING_STATE);
140
-
141
- // Set up handler functions with access to closures
142
- FORM_SECTION_HANDLER_MAP.PERSONAL.handler = () => checkPersonalDataChanged();
143
- FORM_SECTION_HANDLER_MAP.BUSINESS_SERVICES.handler = () => checkBusinessDataChanged();
144
- FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING.handler = () => checkContactDataChanged();
145
- FORM_SECTION_HANDLER_MAP.DIRECTORY_OPT_OUT.handler = () => checkDirectoryOptOutDataChanged();
146
- FORM_SECTION_HANDLER_MAP.WEBSITE_OPT_OUT.handler = () => checkWebsiteOptOutDataChanged();
147
-
148
- const showUnauthorizedState = () => {
149
- console.log('❌ Unauthorized');
150
- _$w('#mainMultiStateBox').changeState(MAIN_STATE_BOX_STATES.UNAUTHORIZED_STATE);
151
- };
152
-
153
- let memberData, isValid, isStudent, membersExternalPortalUrl;
154
-
155
- // Main initialization
156
- const queryParams = await wixLocation.query();
157
- const memberTokenId = queryParams.token;
158
- const baseUrl = await wixLocation.baseUrl();
159
-
160
- console.log('memberTokenId', memberTokenId);
161
-
162
- if (!memberTokenId) {
163
- showUnauthorizedState();
164
- return;
165
- }
166
-
167
- try {
168
- const {
169
- memberData: { isStudent: _isStudent, ...memberDataResponse },
170
- isValid: isValidResponse,
171
- membersExternalPortalUrl: _membersExternalPortalUrl,
172
- } = await validateMemberToken(memberTokenId);
173
- memberData = memberDataResponse;
174
- isValid = isValidResponse;
175
- isStudent = _isStudent;
176
- membersExternalPortalUrl = _membersExternalPortalUrl;
177
- } catch (error) {
178
- console.error(`Error in validateMemberToken memberTokenId : ${memberTokenId}`, error);
179
- _$w('#mainMultiStateBox').changeState(MAIN_STATE_BOX_STATES.ERROR_STATE);
180
- return;
181
- }
182
-
183
- console.log('memberData frontend', memberData);
184
- if (!isValid) {
185
- showUnauthorizedState();
186
- return;
187
- }
188
-
189
- console.log('✅ Authorized 2', { memberTokenId });
190
- _$w('#loginButton2').hide();
191
- _$w('#goBackButton').show();
192
-
193
- _$w('#goBackButton').onClick(async () => {
194
- try {
195
- const isFormHasUnsavedChanges = Object.values(formHasUnsavedChanges).some(Boolean);
196
- if (isFormHasUnsavedChanges) {
197
- wixWindow.openLightbox(LIGHTBOX_NAMES.SAVE_ALERT, {
198
- membersExternalPortalUrl,
199
- });
200
- } else {
201
- await wixLocation.to(membersExternalPortalUrl);
202
- }
203
- } catch (error) {
204
- console.error('Logout failed:', error);
205
- }
206
- });
207
- itemMemberObj = memberData;
208
- originalMemberData = JSON.parse(JSON.stringify(memberData));
209
- // Initialize selectedServices based on memberData
210
- selectedServices = Array.isArray(itemMemberObj.areasOfPractices)
211
- ? itemMemberObj.areasOfPractices.map(label => ({ _id: generateId(), label: String(label) }))
212
- : [];
213
-
214
- _$w('#mainMultiStateBox').changeState(MAIN_STATE_BOX_STATES.FORM_STATE);
215
- init();
216
- setupStepTrackingWrapper();
217
- //initially disable save buttons
218
- _$w('#savePersonalButton').disable();
219
- _$w('#saveBusinessButton').disable();
220
- _$w('#saveContactBookingButton').disable();
221
- onFormDataChanged();
222
-
223
- function setupStepTrackingWrapper() {
224
- const stepPairs = [
225
- {
226
- mark: '#personalDetailsMark',
227
- sing: '#personalDetailsSign',
228
- step: '#personalDetailsStep',
229
- text: '#personalDetailsText',
230
- css: 'personal',
231
- vector: '#personalDetailsVector',
232
- },
233
- {
234
- mark: '#businessServicesMark',
235
- sing: '#businessServicesSign',
236
- step: '#businessServicesStep',
237
- text: '#businessServicesText',
238
- css: 'business',
239
- vector: '#businessServicesVector',
240
- },
241
- {
242
- mark: '#contactMark',
243
- sing: '#contactSign',
244
- step: '#contactStep',
245
- text: '#contactText',
246
- css: 'contact',
247
- vector: '#contactVector',
248
- },
249
- {
250
- mark: '#galleryMark',
251
- sing: '#gallerySign',
252
- step: '#galleryStep',
253
- text: '#galleryText',
254
- css: 'gallery',
255
- vector: '#galleryVector',
256
- },
257
- ];
258
-
259
- setupStepTracking(stepPairs);
260
- }
261
-
262
- function setupStepTracking(stepPairs) {
263
- stepPairs.forEach(({ mark, sing, step, text, css, vector }) => {
264
- _$w(mark).onViewportEnter(() => {
265
- _$w(sing).customClassList.add('current-step');
266
- _$w(step).customClassList.add('highlighted-text');
267
- _$w(text).customClassList.add('highlighted-text');
268
- _$w(vector).customClassList.add('disabeld-step');
269
- _$w('#accordion').customClassList.add(css);
270
- _$w(mark).scrollTo();
271
- });
272
-
273
- _$w(mark).onViewportLeave(() => {
274
- _$w(sing).customClassList.remove('current-step');
275
- _$w(step).customClassList.remove('highlighted-text');
276
- _$w(text).customClassList.remove('highlighted-text');
277
- _$w(vector).customClassList.remove('disabeld-step');
278
- _$w('#accordion').customClassList.remove(css);
279
- });
280
- });
281
- }
282
-
283
- function init() {
284
- const fullProfilePageLink = `${baseUrl}/profile/${itemMemberObj.url}`;
285
- setPersonalDetails(fullProfilePageLink);
286
- setBusinessServices();
287
- setContactBooking(fullProfilePageLink);
288
- initGallery();
289
- }
290
-
291
- function initGallery() {
292
- _$w('#galleryRepeater').onItemReady(handleGalleryItem);
293
- _$w('#uploadGalleryImageButton').onChange(async event => {
294
- const $item = _$w.at(event.context);
295
- const uploadButton = $item('#uploadGalleryImageButton');
296
- if (uploadButton.value.length === 0) return;
297
- try {
298
- const uploadedFiles = await uploadButton.uploadFiles();
299
- // Initialize gallery array if it doesn't exist
300
- if (!itemMemberObj.gallery) {
301
- itemMemberObj.gallery = [];
302
- }
303
-
304
- uploadedFiles.forEach(file => {
305
- itemMemberObj.gallery.unshift({ src: file.fileUrl });
306
- });
307
-
308
- await saveGalleryToCMS();
309
- setGallery();
310
- } catch (error) {
311
- _$w('#uploadFailedText').expand();
312
- setTimeout(() => {
313
- _$w('#uploadFailedText').collapse();
314
- }, 5000);
315
- console.error('Upload failed:', error);
316
- }
317
- });
318
- _$w('#deleteImageButton').onClick(async event => {
319
- const itemId = event.context.itemId;
320
- const itemData = _$w('#galleryRepeater').data.find(item => item._id === itemId);
321
- const result = await wixWindow.openLightbox(LIGHTBOX_NAMES.DELETE_CONFIRM);
322
- if (result && result.toDelete) {
323
- itemMemberObj.gallery = itemMemberObj.gallery.filter(img => img.src !== itemData.image.src);
324
- await saveGalleryToCMS();
325
- setGallery(); // Re-render
326
- }
327
- });
328
- //
329
- setGallery();
330
- }
331
-
332
- function setPersonalDetails(fullProfilePageLink) {
333
- _$w('#firstNameInput').value = itemMemberObj.firstName || '';
334
- _$w('#lastNameInput').value = itemMemberObj.lastName || '';
335
-
336
- _$w('#slugInput').value = itemMemberObj.url || '';
337
-
338
- isSlugValid = true;
339
-
340
- _$w(SLUG_FLAGS.VALID).collapse();
341
- _$w(SLUG_FLAGS.INVALID).collapse();
342
-
343
- _$w('#profileLink').text = fullProfilePageLink;
344
- _$w('#profileLink').link = fullProfilePageLink;
345
- _$w('#profileLink').target = '_blank';
346
- _$w('#licenceNoText').text = (itemMemberObj.licenses || [])
347
- .map(val => val.license)
348
- .filter(Boolean)
349
- .join(', ');
350
-
351
- const handleIsStudent = () => {
352
- if (isStudent) {
353
- _$w('#optCheckbox').disable();
354
- _$w('#optCheckbox').checked = false;
355
- _$w('#optCheckbox').customClassList.add('disabled-text');
356
- _$w('#optCheckbox').customClassList.add('disabled-checkbox');
357
- } else {
358
- _$w('#optCheckbox').enable();
359
- _$w('#optCheckbox').checked = !itemMemberObj.optOut;
360
- }
361
- };
362
-
363
- handleIsStudent();
364
- _$w('#optWebsiteCheckbox').checked = itemMemberObj.showWixUrl;
365
- handleOptWebsiteCheckboxEnable(itemMemberObj.showWebsite);
366
- toggleFreeWebsiteText(itemMemberObj.showWixUrl);
367
-
368
- setupOptOutCheckbox(
369
- '#optCheckbox',
370
- '#optConfirmationBox',
371
- '#yesOptButton',
372
- '#cancelOptButton',
373
- confirmed => handleOptConfirmation(confirmed, '#optCheckbox', '#optConfirmationBox', 'optOut')
374
- );
375
-
376
- setupOptOutCheckbox(
377
- '#optWebsiteCheckbox',
378
- '#optWebsiteConfirmationBox',
379
- '#yesOptWebsiteButton',
380
- '#cancelOptwebsiteButton',
381
- confirmed =>
382
- handleOptConfirmation(
383
- confirmed,
384
- '#optWebsiteCheckbox',
385
- '#optWebsiteConfirmationBox',
386
- 'showWixUrl'
387
- )
388
- );
389
- }
390
-
391
- function handleOptWebsiteCheckboxEnable(showExistingWebsite) {
392
- if (showExistingWebsite) {
393
- _$w('#optWebsiteCheckbox').checked = false;
394
- _$w('#optWebsiteCheckbox').customClassList.add('disabled-text');
395
- _$w('#optWebsiteCheckbox').customClassList.add('disabled-checkbox');
396
- _$w('#optWebsiteCheckbox').disable();
397
- } else {
398
- _$w('#optWebsiteCheckbox').customClassList.remove('disabled-text');
399
- _$w('#optWebsiteCheckbox').customClassList.remove('disabled-checkbox');
400
- _$w('#optWebsiteCheckbox').enable();
401
- }
402
- }
403
-
404
- function setupOptOutCheckbox(
405
- checkboxId,
406
- confirmationBoxId,
407
- confirmBtnId,
408
- cancelBtnId,
409
- confirmCallback
410
- ) {
411
- const checkbox = _$w(checkboxId);
412
- const box = _$w(confirmationBoxId);
413
-
414
- checkbox.onChange(e => {
415
- if (!e.target.checked) {
416
- box.expand();
417
- checkbox.disable();
418
- checkbox.customClassList.add('disabled-text');
419
- } else {
420
- confirmCallback(true);
421
- }
422
- let sectionHandlerType;
423
- if (checkboxId === '#optCheckbox') {
424
- sectionHandlerType = FORM_SECTION_HANDLER_MAP.DIRECTORY_OPT_OUT;
425
- } else if (checkboxId === '#optWebsiteCheckbox') {
426
- sectionHandlerType = FORM_SECTION_HANDLER_MAP.WEBSITE_OPT_OUT;
427
- }
428
- checkFormChanges(sectionHandlerType);
429
- });
430
-
431
- _$w(confirmBtnId).onClick(() => confirmCallback(true));
432
- _$w(cancelBtnId).onClick(() => confirmCallback(false));
433
- }
434
-
435
- function toggleFreeWebsiteText(isFreeWebsiteEnabled) {
436
- if (isFreeWebsiteEnabled) {
437
- _$w('#freeWebsiteText').text = FREE_WEBSITE_TEXT_STATES.ENABLED;
438
- } else {
439
- _$w('#freeWebsiteText').text = FREE_WEBSITE_TEXT_STATES.DISABLED;
440
- }
441
- }
442
-
443
- async function handleOptConfirmation(confirmed, optCheckbox, optConfirmationBox, field) {
444
- const checkbox = _$w(optCheckbox);
445
- const box = _$w(optConfirmationBox);
446
- if (confirmed) {
447
- const toSaveOptValue = optCheckbox === '#optCheckbox' ? !checkbox.checked : checkbox.checked;
448
- const formData = {
449
- ...itemMemberObj,
450
- [field]: toSaveOptValue,
451
- };
452
-
453
- await saveData(formData);
454
- if (field === 'showWixUrl') {
455
- const showExistingUrl = _$w('#showExsistingUrlCheckbox').checked;
456
- if (toSaveOptValue) {
457
- if (!showExistingUrl) {
458
- _$w('#showUrlWixCheckbox').checked = true;
459
- }
460
- _$w('#showUrlWixCheckbox').enable();
461
- } else {
462
- _$w('#showUrlWixCheckbox').checked = false;
463
- _$w('#showUrlWixCheckbox').disable();
464
- }
465
- toggleFreeWebsiteText(toSaveOptValue);
466
- }
467
- }
468
-
469
- box.collapse();
470
- checkbox.enable();
471
- checkbox.customClassList.remove('disabled-text');
472
-
473
- if (!confirmed) {
474
- checkbox.checked = !checkbox.checked;
475
- }
476
- const section =
477
- field === 'showWixUrl'
478
- ? FORM_SECTION_HANDLER_MAP.WEBSITE_OPT_OUT.section
479
- : FORM_SECTION_HANDLER_MAP.DIRECTORY_OPT_OUT.section;
480
- formHasUnsavedChanges[section] = false;
481
- }
482
-
483
- function onFormDataChanged() {
484
- const CHANGE_EVENTS = {
485
- ON_CHANGE: 'onChange',
486
- ON_INPUT: 'onInput',
487
- };
488
- const elements = {
489
- $firstNameInput: _$w('#firstNameInput'),
490
- $lastNameInput: _$w('#lastNameInput'),
491
- $businessNameCheckbox: _$w('#businessNameCheckbox'),
492
- $yearJoinedcheckbox: _$w('#yearJoinedcheckbox'),
493
- $aboutInput: _$w('#aboutInput'),
494
- $businessNameInput: _$w('#businessNameInput'),
495
- $uploadProfileButton: _$w('#uploadProfileButton'),
496
- $uploadLogoButton: _$w('#uploadLogoButton'),
497
- $uploadBannerButton: _$w('#uploadBannerButton'),
498
- $showContactFormCheckbox: _$w('#showCotactFormCheckbox'),
499
- $contactFormEmailInput: _$w('#contactFormEmailInput'),
500
- $schedulingLinkInput: _$w('#schedulingLinkInput'),
501
- $UrlInput: _$w('#UrlInput'),
502
- $slugInput: _$w('#slugInput'),
503
- };
504
-
505
- const formChangeEventBindings = {
506
- [FORM_SECTION_HANDLER_MAP.PERSONAL.section]: [
507
- { $elem: elements.$firstNameInput, changeEvent: CHANGE_EVENTS.ON_INPUT },
508
- { $elem: elements.$lastNameInput, changeEvent: CHANGE_EVENTS.ON_INPUT },
509
- { $elem: elements.$slugInput, changeEvent: CHANGE_EVENTS.ON_INPUT },
510
- ],
511
- [FORM_SECTION_HANDLER_MAP.BUSINESS_SERVICES.section]: [
512
- { $elem: elements.$businessNameCheckbox, changeEvent: CHANGE_EVENTS.ON_CHANGE },
513
- { $elem: elements.$yearJoinedcheckbox, changeEvent: CHANGE_EVENTS.ON_CHANGE },
514
- { $elem: elements.$aboutInput, changeEvent: CHANGE_EVENTS.ON_CHANGE },
515
- { $elem: elements.$businessNameInput, changeEvent: CHANGE_EVENTS.ON_INPUT },
516
- { $elem: elements.$uploadProfileButton, changeEvent: CHANGE_EVENTS.ON_CHANGE },
517
- { $elem: elements.$uploadLogoButton, changeEvent: CHANGE_EVENTS.ON_CHANGE },
518
- { $elem: elements.$uploadBannerButton, changeEvent: CHANGE_EVENTS.ON_CHANGE },
519
- ],
520
- [FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING.section]: [
521
- { $elem: elements.$showContactFormCheckbox, changeEvent: CHANGE_EVENTS.ON_CHANGE },
522
- { $elem: elements.$schedulingLinkInput, changeEvent: CHANGE_EVENTS.ON_INPUT },
523
- { $elem: elements.$UrlInput, changeEvent: CHANGE_EVENTS.ON_INPUT },
524
- ],
525
- };
526
- Object.keys(formChangeEventBindings).forEach(section => {
527
- formChangeEventBindings[section].forEach(({ $elem, changeEvent }) => {
528
- $elem[changeEvent](() => {
529
- const handlerMap = Object.values(FORM_SECTION_HANDLER_MAP).find(
530
- handlerMap => handlerMap.section === section
531
- );
532
- checkFormChanges(handlerMap);
533
- if (section === FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING.section) {
534
- setTimeout(syncOtherWebsiteUrlValidity, 0);
535
- }
536
- });
537
- });
538
- });
539
-
540
- elements.$contactFormEmailInput.onCustomValidation((value, reject) => {
541
- if (!value) {
542
- reject(CONTACT_EMAIL_VALIDATION_MESSAGES.REQUIRED);
543
- return;
544
- }
545
- // When value is non-empty, ignore stale "required" state (from when field was empty).
546
- // Blur can run validation before async isEmailAlreadyUsed completes.
547
- if (
548
- !ContactEmailValidity.valid &&
549
- ContactEmailValidity.validationMessage !== CONTACT_EMAIL_VALIDATION_MESSAGES.REQUIRED
550
- ) {
551
- reject(ContactEmailValidity.validationMessage);
552
- return;
553
- }
554
- });
555
- // Helper to force trigger contact email custom validation programmatically (onCustomValidation
556
- // does not run with async validators, so we trigger it manually on input).
557
- const forceTriggerContactEmailCustomValidation = value => {
558
- elements.$contactFormEmailInput.value = value;
559
- };
560
-
561
- const setContactEmailInvalid = (message, value) => {
562
- ContactEmailValidity.valid = false;
563
- ContactEmailValidity.validationMessage = message;
564
- forceTriggerContactEmailCustomValidation(value);
565
- };
566
-
567
- elements.$contactFormEmailInput.onInput(event => {
568
- const value = event.target.value;
569
- _$w('#saveContactBookingButton').disable();
570
-
571
- if (contactEmailValidationTimeout.validation) {
572
- clearTimeout(contactEmailValidationTimeout.validation);
573
- }
574
-
575
- if (!value) {
576
- setContactEmailInvalid(CONTACT_EMAIL_VALIDATION_MESSAGES.REQUIRED, value);
577
- checkFormChanges(FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING);
578
- return;
579
- }
580
-
581
- const validationId = ++currentContactEmailValidationId;
582
- contactEmailValidationTimeout.validation = setTimeout(async () => {
583
- try {
584
- if (validationId !== currentContactEmailValidationId) {
585
- return;
586
- }
587
-
588
- const isAlreadyUsed = await isEmailAlreadyUsed(value, itemMemberObj.memberId);
589
- if (validationId !== currentContactEmailValidationId) {
590
- return;
591
- }
592
-
593
- if (isAlreadyUsed) {
594
- setContactEmailInvalid(CONTACT_EMAIL_VALIDATION_MESSAGES.ALREADY_TAKEN, value);
595
- } else {
596
- ContactEmailValidity.valid = true;
597
- ContactEmailValidity.validationMessage = '';
598
- forceTriggerContactEmailCustomValidation(value);
599
- }
600
- } catch (error) {
601
- console.error('Email validation error:', error);
602
- if (validationId === currentContactEmailValidationId) {
603
- setContactEmailInvalid(CONTACT_EMAIL_VALIDATION_MESSAGES.ERROR, value);
604
- }
605
- } finally {
606
- if (validationId === currentContactEmailValidationId) {
607
- checkFormChanges(FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING);
608
- }
609
- }
610
- }, 500);
611
- });
612
-
613
- _$w('#slugInput').onInput(event => {
614
- _$w('#savePersonalButton').disable();
615
- const slug = event.target.value;
616
-
617
- isSlugValid = false;
618
-
619
- if (slugValidationTimeout.slugValidation) {
620
- clearTimeout(slugValidationTimeout.slugValidation);
621
- }
622
-
623
- const validationId = ++currentSlugValidationId;
624
-
625
- slugValidationTimeout.slugValidation = setTimeout(async () => {
626
- try {
627
- if (validationId !== currentSlugValidationId) {
628
- return;
629
- }
630
-
631
- const result = await validateSlugRealTime(slug);
632
-
633
- if (validationId === currentSlugValidationId) {
634
- isSlugValid = result.isValid;
635
- checkFormChanges(FORM_SECTION_HANDLER_MAP.PERSONAL);
636
- }
637
- } catch (error) {
638
- console.error('Slug validation error:', error);
639
- if (validationId === currentSlugValidationId) {
640
- isSlugValid = false;
641
- checkFormChanges(FORM_SECTION_HANDLER_MAP.PERSONAL);
642
- }
643
- }
644
- }, 800);
645
- });
646
- }
647
-
648
- function syncOtherWebsiteUrlValidity() {
649
- const showExistingUrl = _$w('#showExsistingUrlCheckbox').checked;
650
- const $urlInput = _$w('#UrlInput');
651
- $urlInput.required = showExistingUrl;
652
- if (showExistingUrl) {
653
- $urlInput.updateValidityIndication();
654
- } else {
655
- $urlInput.resetValidityIndication();
656
- }
657
- }
658
-
659
- function checkFormChanges(formSectionHandler) {
660
- let isFormDataChanged = false;
661
- const toggleSaveDataButton = (formDataType, isFormDataChanged) => {
662
- const saveButtonMap = {
663
- [FORM_SECTION_HANDLER_MAP.PERSONAL.section]: '#savePersonalButton',
664
- [FORM_SECTION_HANDLER_MAP.BUSINESS_SERVICES.section]: '#saveBusinessButton',
665
- [FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING.section]: '#saveContactBookingButton',
666
- };
667
-
668
- const buttonSelector = saveButtonMap[formDataType];
669
- if (!buttonSelector) {
670
- throw new Error(`No save button defined for form section: ${formDataType}`);
671
- }
672
- const $saveDataButton = _$w(buttonSelector);
673
-
674
- let isUrlValid = true,
675
- isEmailValid = true,
676
- isNameValid = true,
677
- isSlugValidLocal = true;
678
- if (formDataType === FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING.section) {
679
- isEmailValid = _$w('#contactFormEmailInput').valid;
680
- const showExistingUrl = _$w('#showExsistingUrlCheckbox').checked;
681
- const otherWebsiteValue = (_$w('#UrlInput').value || '').trim();
682
- const isOtherWebsiteRequiredValid =
683
- !showExistingUrl || (otherWebsiteValue !== '' && !isNotValidUrl(otherWebsiteValue));
684
- isUrlValid =
685
- isOtherWebsiteRequiredValid &&
686
- !isNotValidUrl(_$w('#UrlInput').value) &&
687
- !isNotValidUrl(_$w('#schedulingLinkInput').value);
688
- syncOtherWebsiteUrlValidity();
689
- }
690
- if (formDataType === FORM_SECTION_HANDLER_MAP.PERSONAL.section) {
691
- isNameValid = _$w('#firstNameInput').valid && _$w('#lastNameInput').valid;
692
- isSlugValidLocal = isSlugValid;
693
- }
694
-
695
- if (isFormDataChanged && isUrlValid && isEmailValid && isNameValid && isSlugValidLocal) {
696
- $saveDataButton.enable();
697
- } else {
698
- $saveDataButton.disable();
699
- }
700
- };
701
-
702
- if (formSectionHandler) {
703
- const { section, handler } = formSectionHandler;
704
- isFormDataChanged = handler();
705
- formHasUnsavedChanges[section] = isFormDataChanged;
706
- if (
707
- [
708
- FORM_SECTION_HANDLER_MAP.PERSONAL.section,
709
- FORM_SECTION_HANDLER_MAP.BUSINESS_SERVICES.section,
710
- FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING.section,
711
- ].includes(section)
712
- ) {
713
- toggleSaveDataButton(section, isFormDataChanged);
714
- }
715
- } else {
716
- Object.values(FORM_SECTION_HANDLER_MAP).forEach(({ section, handler }) => {
717
- formHasUnsavedChanges[section] = handler();
718
- });
719
- }
720
- }
721
-
722
- function checkWebsiteOptOutDataChanged() {
723
- const currentWebsiteOptInData = _$w('#optWebsiteCheckbox').checked;
724
- const originalWebsiteOptInData = originalMemberData.showWixUrl;
725
- return !_.isEqual(currentWebsiteOptInData, originalWebsiteOptInData);
726
- }
727
-
728
- function checkDirectoryOptOutDataChanged() {
729
- const currentDirectoryOptOutData = _$w('#optCheckbox').checked;
730
- const originalDirectoryOptOutData = !originalMemberData.optOut;
731
- return !_.isEqual(currentDirectoryOptOutData, originalDirectoryOptOutData);
732
- }
733
-
734
- function checkBusinessDataChanged() {
735
- const currentBusinessData = getBusinessAndServicesData();
736
- const originalBusinessData = {
737
- showBusinessName: originalMemberData.showBusinessName,
738
- businessName: originalMemberData.businessName,
739
- showABMP: originalMemberData.showABMP,
740
- aboutService: originalMemberData.aboutService,
741
- profileImage: originalMemberData.profileImage,
742
- logoImage: originalMemberData.logoImage,
743
- bannerImages: originalMemberData.bannerImages || [],
744
- areasOfPractices: originalMemberData.areasOfPractices || [],
745
- testimonial: originalMemberData.testimonial || [],
746
- };
747
- return !_.isEqual(currentBusinessData, originalBusinessData);
748
- }
749
-
750
- function checkContactDataChanged() {
751
- const currentContactData = getContactAndBookingData();
752
- const originalContactData = {
753
- showContactForm: originalMemberData.showContactForm,
754
- contactFormEmail: originalMemberData.contactFormEmail,
755
- toShowPhone: originalMemberData.toShowPhone,
756
- bookingUrl: originalMemberData.bookingUrl,
757
- website: originalMemberData.website,
758
- showWebsite: originalMemberData.showWebsite,
759
- showWixUrl: originalMemberData.showWixUrl,
760
- addressDisplayOption: originalMemberData.addressDisplayOption,
761
- addresses: originalMemberData.addresses,
762
- phones: Array.isArray(originalMemberData.phones) ? originalMemberData.phones : [],
763
- };
764
- // Normalize so empty string and undefined are treated the same
765
- const str = v => (v != null ? v : '').toString().trim();
766
- const normalizeContactForCompare = data => ({
767
- ...data,
768
- website: str(data.website),
769
- bookingUrl: str(data.bookingUrl),
770
- contactFormEmail: str(data.contactFormEmail),
771
- });
772
- return !_.isEqual(
773
- normalizeContactForCompare(currentContactData),
774
- normalizeContactForCompare(originalContactData)
775
- );
776
- }
777
-
778
- function setBusinessServices() {
779
- _$w('#businessNameText').text = itemMemberObj.businessName || DEFAULT_BUSINESS_NAME_TEXT;
780
- _$w('#businessNameCheckbox').checked = itemMemberObj.showBusinessName;
781
- _$w('#yearJoinedcheckbox').checked = itemMemberObj.showABMP;
782
- _$w('#aboutInput').value = itemMemberObj.aboutService;
783
- _$w('#businessNameInput').value = itemMemberObj.businessName;
784
- _$w('#clearBusinessNameBtn').onClick(() => {
785
- _$w('#businessNameInput').value = '';
786
- checkFormChanges(FORM_SECTION_HANDLER_MAP.BUSINESS_SERVICES);
787
- });
788
-
789
- // Get memberships array
790
- const memberships = Array.isArray(itemMemberObj.memberships) ? itemMemberObj.memberships : [];
791
- // Find Site Association Member Since
792
- const siteAssociationMemberSince = memberships.find(m => m.isSiteAssociation)?.membersince;
793
- // Set yearJoinedText
794
- if (siteAssociationMemberSince) {
795
- _$w('#yearJoinedText').text = siteAssociationMemberSince;
796
- } else {
797
- _$w('#yearJoinedText').text = 'Year joined not provided';
798
- }
799
-
800
- uploadImageFromLightbox();
801
- setupImageUploadAndDeleteHandlers();
802
- setupServiceSelection();
803
- setInterestData();
804
- setupTestimonials();
805
-
806
- // Initialize areasOfPractices array if it doesn't exist
807
- if (!itemMemberObj.areasOfPractices) {
808
- itemMemberObj.areasOfPractices = [];
809
- }
810
-
811
- if (Array.isArray(itemMemberObj.areasOfPractices)) {
812
- selectedServices = itemMemberObj.areasOfPractices.map(label => ({
813
- _id: generateId(),
814
- label: String(label),
815
- }));
816
- renderServices();
817
- }
818
-
819
- displayExistingImagesFromCMS();
820
-
821
- _$w('#savePersonalButton').onClick(savePersonalDetails);
822
- _$w('#saveBusinessButton').onClick(saveBusinessServices);
823
- _$w('#servicesRepeater').onItemReady(($item, itemData) => {
824
- $item('#serviceNameText').text = itemData.label;
825
- });
826
- }
827
-
828
- function uploadImage(uploadButton, imageKey, updateUI) {
829
- _$w(uploadButton).onChange(async () => {
830
- if (_$w(uploadButton).value?.length > 0) {
831
- try {
832
- const uploadedFiles = await _$w(uploadButton).uploadFiles();
833
- uploadedFiles.forEach(file => {
834
- uploadedImages[imageKey] = file.fileUrl;
835
- updateUI(file);
836
- });
837
- } catch (error) {
838
- console.error(`File upload error: ${error.errorCode}`, error.errorDescription);
839
- }
840
- checkFormChanges(FORM_SECTION_HANDLER_MAP.BUSINESS_SERVICES);
841
- }
842
- });
843
- }
844
-
845
- function uploadImageFromLightbox() {
846
- _$w('#bannerLightboxButton').onClick(async () => {
847
- const returnedImage = await wixWindow.openLightbox(LIGHTBOX_NAMES.SELECT_BANNER_IMAGES);
848
- console.log('uploadedImages', returnedImage);
849
-
850
- if (returnedImage && returnedImage.image) {
851
- console.log('Image returned from lightbox:', returnedImage);
852
-
853
- // Update stored image
854
- uploadedImages.bannerImage = returnedImage.image;
855
- checkFormChanges(FORM_SECTION_HANDLER_MAP.BUSINESS_SERVICES);
856
- _$w('#bannerImage').src = returnedImage.image;
857
- _$w('#bannerImageName').text = extractFileName(returnedImage.image);
858
- _$w('#bannerImageContainer').expand();
859
- }
860
- });
861
- }
862
-
863
- function extractFileName(fileUrl) {
864
- try {
865
- const url = new URL(fileUrl);
866
- const pathParts = url.pathname.split('/');
867
- return pathParts[pathParts.length - 1] || '';
868
- } catch {
869
- return '';
870
- }
871
- }
872
-
873
- function setupImageUploadAndDeleteHandlers() {
874
- uploadImage('#uploadProfileButton', 'profileImage', file => {
875
- _$w('#profileImage').src = file.fileUrl;
876
- _$w('#profileImageName').text = formatFileName(file.fileName);
877
- _$w('#profileImageContainer').expand();
878
- });
879
-
880
- uploadImage('#uploadLogoButton', 'logoImage', file => {
881
- _$w('#logoImage').src = file.fileUrl;
882
- _$w('#logoImageName').text = file.fileName;
883
- _$w('#logoImageContainer').expand();
884
- });
885
-
886
- uploadImage('#uploadBannerButton', 'bannerImage', file => {
887
- _$w('#bannerImage').src = file.fileUrl;
888
- _$w('#bannerImageName').text = file.fileName;
889
- _$w('#bannerImageContainer').expand();
890
- });
891
-
892
- setupDeleteHandler(
893
- '#deleteProfileImage',
894
- '#profileImage',
895
- '#profileImageName',
896
- '#profileImageContainer',
897
- 'profileImage',
898
- '#uploadProfileButton'
899
- );
900
- setupDeleteHandler(
901
- '#deleteLogoImage',
902
- '#logoImage',
903
- '#logoImageName',
904
- '#logoImageContainer',
905
- 'logoImage',
906
- '#uploadLogoButton'
907
- );
908
- setupDeleteHandler(
909
- '#deleteBannerImage',
910
- '#bannerImage',
911
- '#bannerImageName',
912
- '#bannerImageContainer',
913
- 'bannerImage',
914
- '#uploadBannerButton'
915
- );
916
- }
917
-
918
- function setupDeleteHandler(deleteBtn, imgId, nameId, containerId, imageKey, uploadBtnId) {
919
- _$w(deleteBtn).onClick(async () => {
920
- const result = await wixWindow.openLightbox(LIGHTBOX_NAMES.DELETE_CONFIRM);
921
-
922
- if (result && result.toDelete) {
923
- _$w(imgId).src = '';
924
- _$w(nameId).text = '';
925
- _$w(containerId).collapse();
926
- uploadedImages[imageKey] = '';
927
- checkFormChanges(FORM_SECTION_HANDLER_MAP.BUSINESS_SERVICES);
928
- _$w(uploadBtnId).reset();
929
- }
930
- });
931
- }
932
-
933
- function displayExistingImagesFromCMS() {
934
- const imageMap = [
935
- {
936
- key: 'profileImage',
937
- imageSelector: '#profileImage',
938
- nameSelector: '#profileImageName',
939
- containerSelector: '#profileImageContainer',
940
- },
941
- {
942
- key: 'logoImage',
943
- imageSelector: '#logoImage',
944
- nameSelector: '#logoImageName',
945
- containerSelector: '#logoImageContainer',
946
- },
947
- {
948
- key: 'bannerImages',
949
- imageSelector: '#bannerImage',
950
- nameSelector: '#bannerImageName',
951
- containerSelector: '#bannerImageContainer',
952
- isArray: true,
953
- },
954
- ];
955
-
956
- imageMap.forEach(({ key, imageSelector, nameSelector, containerSelector, isArray }) => {
957
- const imageValue = isArray
958
- ? Array.isArray(itemMemberObj[key]) && itemMemberObj[key].length > 0
959
- ? itemMemberObj[key][0]
960
- : null
961
- : itemMemberObj[key];
962
-
963
- if (imageValue && imageValue?.trim()) {
964
- // Only set profile image if it's Wix-hosted; other images will always be wix url
965
- const isProfileImage = imageSelector === '#profileImage';
966
- const shouldSetImage = !isProfileImage || isWixHostedImage(imageValue);
967
-
968
- if (shouldSetImage) {
969
- _$w(imageSelector).src = imageValue;
970
- }
971
- _$w(nameSelector).text = formatFileName(extractFileName(imageValue));
972
- _$w(containerSelector).expand();
973
- uploadedImages[key === 'bannerImages' ? 'bannerImage' : key] = imageValue;
974
- }
975
- });
976
- }
977
-
978
- async function setInterestData() {
979
- const interestsData = await getInterestAll();
980
-
981
- _$w('#removeServiceButton').onClick(async event => {
982
- const itemId = event.context.itemId;
983
- const result = await wixWindow.openLightbox(LIGHTBOX_NAMES.DELETE_CONFIRM);
984
-
985
- if (result && result.toDelete) {
986
- selectedServices = selectedServices.filter(service => service._id !== itemId);
987
- renderServices();
988
- checkFormChanges(FORM_SECTION_HANDLER_MAP.BUSINESS_SERVICES);
989
- }
990
- });
991
-
992
- if (Array.isArray(interestsData) && interestsData.length > 0) {
993
- const formattedData = interestsData.map((val, index) => ({
994
- _id: String(index),
995
- value: val,
996
- }));
997
-
998
- _$w('#repeaterInterest').data = formattedData;
999
- _$w('#repeaterInterest').onItemReady(($item, itemData, _index) => {
1000
- $item('#interestText').text = itemData.value;
1001
- });
1002
- }
1003
- }
1004
-
1005
- const debounce_fun = _.debounce(async () => {
1006
- const searchValue = _$w('#intrestInput').value.trim().toLowerCase();
1007
- await filterInterests(searchValue);
1008
- }, 250);
1009
-
1010
- function setupServiceSelection() {
1011
- const intrestInput = _$w('#intrestInput');
1012
-
1013
- intrestInput.onClick(() => {
1014
- if (intrestInput.value) {
1015
- intrestInput.onClick(async () => {
1016
- await filterInterests(intrestInput.value);
1017
- });
1018
- } else {
1019
- setInterestData();
1020
- }
1021
- _$w('#containerRepeaterInterest').expand();
1022
- });
1023
-
1024
- intrestInput.onKeyPress(event => {
1025
- debounce_fun();
1026
-
1027
- if (event.key === 'Enter') {
1028
- const typedValue = intrestInput.value.trim();
1029
-
1030
- if (
1031
- typedValue &&
1032
- !selectedServices.some(
1033
- service => service.label.toLowerCase() === typedValue.toLowerCase()
1034
- )
1035
- ) {
1036
- selectedServices.unshift({
1037
- _id: generateId(),
1038
- label: typedValue,
1039
- });
1040
- checkFormChanges(FORM_SECTION_HANDLER_MAP.BUSINESS_SERVICES);
1041
- renderServices();
1042
- intrestInput.value = '';
1043
- _$w('#containerRepeaterInterest').collapse();
1044
- }
1045
- }
1046
- });
1047
-
1048
- _$w('#repeaterInterest').onItemReady(($item, itemData) => {
1049
- $item('#interestText').text = itemData.value;
1050
-
1051
- $item('#intrestItem').onClick(() => {
1052
- if (!selectedServices.some(service => service.label === itemData.value)) {
1053
- selectedServices.unshift({
1054
- _id: generateId(),
1055
- label: itemData.value,
1056
- });
1057
- checkFormChanges(FORM_SECTION_HANDLER_MAP.BUSINESS_SERVICES);
1058
- renderServices();
1059
- _$w('#intrestInput').value = '';
1060
- _$w('#containerRepeaterInterest').collapse();
1061
- }
1062
- });
1063
- });
1064
- }
1065
-
1066
- function renderServices() {
1067
- setupRepeater('#servicesRepeater', [...selectedServices]);
1068
- }
1069
-
1070
- function setupRepeater(repeaterId, data) {
1071
- const repeater = _$w(repeaterId);
1072
- repeater.data = data;
1073
- }
1074
-
1075
- /**
1076
- * Logs user data changes for debugging and investigation purposes
1077
- * @param {string} saveType - Type of save operation (personal, business, contact)
1078
- * @param {Object} beforeData - Data before save
1079
- * @param {Object} afterData - Data after save
1080
- * @param {boolean} success - Whether the save was successful
1081
- */
1082
- function logUserDataChanges(saveType, beforeData, afterData, success) {
1083
- const timestamp = new Date().toISOString();
1084
-
1085
- console.group(
1086
- `User Data Change Log - ${saveType.toUpperCase()} - ${success ? 'SUCCESS' : 'FAILED'}`
1087
- );
1088
- console.log('Change Details:', {
1089
- timestamp,
1090
- saveType,
1091
- success,
1092
- });
1093
-
1094
- console.log('BEFORE Save:', beforeData);
1095
- console.log('AFTER Save:', afterData);
1096
-
1097
- // Calculate and log specific changes
1098
- const changes = {};
1099
- Object.keys(afterData).forEach(key => {
1100
- if (JSON.stringify(beforeData[key]) !== JSON.stringify(afterData[key])) {
1101
- changes[key] = {
1102
- before: beforeData[key],
1103
- after: afterData[key],
1104
- };
1105
- }
1106
- });
1107
-
1108
- if (Object.keys(changes).length > 0) {
1109
- console.log('Specific Changes:', changes);
1110
- } else {
1111
- console.log('No changes detected in data comparison');
1112
- }
1113
-
1114
- console.groupEnd();
1115
- }
1116
-
1117
- async function saveData(formData) {
1118
- // Capture data before save for logging
1119
- const beforeSaveData = JSON.parse(JSON.stringify(itemMemberObj));
1120
-
1121
- const { type, saveData: saved } = await saveRegistrationData(formData, itemMemberObj._id);
1122
-
1123
- if (type === 'success') {
1124
- // Log the successful change
1125
- logUserDataChanges('general', beforeSaveData, saved, true);
1126
-
1127
- itemMemberObj = { ...saved };
1128
- originalMemberData = JSON.parse(JSON.stringify(saved));
1129
- return {
1130
- success: true,
1131
- message: 'The information was saved successfully.',
1132
- };
1133
- } else {
1134
- // Log the failed attempt
1135
- logUserDataChanges('general', beforeSaveData, formData, false);
1136
-
1137
- return {
1138
- success: false,
1139
- message:
1140
- "It looks like something went wrong — the information wasn't saved. Please try again later.",
1141
- };
1142
- }
1143
- }
1144
-
1145
- function isValidSlugFormat(slug) {
1146
- if (!slug || slug.length === 0) return false;
1147
- if (slug.length < 3 || slug.length > 50) return false;
1148
-
1149
- const slugRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]$|^[a-zA-Z0-9]$/;
1150
- return slugRegex.test(slug);
1151
- }
1152
-
1153
- async function validateSlugRealTime(slug) {
1154
- _$w(SLUG_FLAGS.VALID).collapse();
1155
- _$w(SLUG_FLAGS.INVALID).collapse();
1156
-
1157
- const trimmedSlug = slug.trim();
1158
-
1159
- if (!isValidSlugFormat(trimmedSlug)) {
1160
- _$w('#invalidSlugMessage').text = SLUG_MESSAGES.INVALID_FORMAT;
1161
- _$w(SLUG_FLAGS.INVALID).expand();
1162
- return { isValid: false };
1163
- }
1164
-
1165
- if (trimmedSlug === (originalMemberData.url || '')) {
1166
- _$w(SLUG_FLAGS.VALID).collapse();
1167
- _$w(SLUG_FLAGS.INVALID).collapse();
1168
- return { isValid: true };
1169
- }
1170
-
1171
- try {
1172
- const result = await checkUrlUniqueness(trimmedSlug, itemMemberObj.memberId);
1173
- const isUnique = result.isUnique;
1174
-
1175
- if (isUnique) {
1176
- _$w(SLUG_FLAGS.VALID).expand();
1177
- _$w(SLUG_FLAGS.INVALID).collapse();
1178
- return { isValid: true };
1179
- } else {
1180
- _$w('#invalidSlugMessage').text = SLUG_MESSAGES.TAKEN;
1181
- _$w(SLUG_FLAGS.INVALID).expand();
1182
- _$w(SLUG_FLAGS.VALID).collapse();
1183
- return { isValid: false };
1184
- }
1185
- } catch (error) {
1186
- console.error('Error checking slug uniqueness:', error);
1187
- _$w('#invalidSlugMessage').text = SLUG_MESSAGES.ERROR;
1188
- _$w(SLUG_FLAGS.INVALID).expand();
1189
- _$w(SLUG_FLAGS.VALID).collapse();
1190
- return { isValid: false };
1191
- }
1192
- }
1193
-
1194
- function getPersonalData() {
1195
- const firstName = _$w('#firstNameInput').value.trim();
1196
- const lastName = _$w('#lastNameInput').value.trim();
1197
- const fullName = `${firstName} ${lastName}`.trim();
1198
- const url = _$w('#slugInput').value.trim();
1199
-
1200
- return {
1201
- firstName,
1202
- lastName,
1203
- fullName,
1204
- url,
1205
- };
1206
- }
1207
-
1208
- function checkPersonalDataChanged() {
1209
- const currentPersonalData = getPersonalData();
1210
- const originalPersonalData = {
1211
- firstName: originalMemberData.firstName || '',
1212
- lastName: originalMemberData.lastName || '',
1213
- fullName: originalMemberData.fullName || '',
1214
- url: originalMemberData.url || '',
1215
- };
1216
- return !_.isEqual(currentPersonalData, originalPersonalData);
1217
- }
1218
-
1219
- function getBusinessAndServicesData() {
1220
- const getCurrentTestimonials = () =>
1221
- _$w('#testimonialRepeater')
1222
- .data.filter(item => item.isAdd === false)
1223
- .map(item => item.text)
1224
- .filter(Boolean) || itemMemberObj.testimonial;
1225
-
1226
- return {
1227
- showBusinessName: _$w('#businessNameCheckbox').checked,
1228
- businessName: _$w('#businessNameInput').value,
1229
- showABMP: _$w('#yearJoinedcheckbox').checked,
1230
- profileImage: uploadedImages.profileImage,
1231
- logoImage: uploadedImages.logoImage,
1232
- bannerImages: uploadedImages.bannerImage ? [uploadedImages.bannerImage] : [],
1233
- areasOfPractices: selectedServices.map(service => service.label),
1234
- aboutService: _$w('#aboutInput').value,
1235
- testimonial: getCurrentTestimonials(),
1236
- };
1237
- }
1238
-
1239
- async function savePersonalDetails() {
1240
- const beforeData = JSON.parse(JSON.stringify(itemMemberObj));
1241
- const personalChanges = getPersonalData();
1242
- const originalUrl = beforeData.url;
1243
-
1244
- // Only send personal fields + required identifiers (partial update)
1245
- const formData = {
1246
- _id: itemMemberObj._id,
1247
- memberId: itemMemberObj.memberId,
1248
- ...personalChanges,
1249
- };
1250
-
1251
- trackClick({
1252
- pageName: PAGE_NAME,
1253
- buttonName: BUTTON_NAMES.PERSONAL,
1254
- data: formData,
1255
- });
1256
-
1257
- // Log the specific personal data changes
1258
- console.group('Personal Details Save Attempt');
1259
- console.log('Current Data:', beforeData);
1260
- console.log('Changes Being Applied:', personalChanges);
1261
- console.log('Final Form Data (partial):', formData);
1262
- console.groupEnd();
1263
-
1264
- const result = await saveData(formData);
1265
-
1266
- if (result.success) {
1267
- formHasUnsavedChanges[FORM_SECTION_HANDLER_MAP.PERSONAL.section] = false;
1268
-
1269
- if (personalChanges.url && personalChanges.url !== originalUrl) {
1270
- const newProfileLink = `${baseUrl}/profile/${personalChanges.url}`;
1271
- console.log('🔗 Updating profile link:', {
1272
- originalUrl,
1273
- newUrl: personalChanges.url,
1274
- newProfileLink,
1275
- });
1276
- _$w('#profileLink').text = newProfileLink;
1277
- _$w('#profileLink').link = newProfileLink;
1278
- _$w('#urlWebsiteText').text = newProfileLink;
1279
-
1280
- _$w(SLUG_FLAGS.VALID).collapse();
1281
- _$w(SLUG_FLAGS.INVALID).collapse();
1282
- }
1283
-
1284
- _$w('#savePersonalButton').disable();
1285
- }
1286
-
1287
- handleSaveDataFeedback(_$w('#personalMessage'), result.message);
1288
- }
1289
-
1290
- async function saveBusinessServices() {
1291
- const beforeData = JSON.parse(JSON.stringify(itemMemberObj));
1292
- const businessChanges = getBusinessAndServicesData();
1293
-
1294
- // Only send business fields + required identifiers (partial update)
1295
- const formData = {
1296
- _id: itemMemberObj._id,
1297
- memberId: itemMemberObj.memberId,
1298
- ...businessChanges,
1299
- };
1300
-
1301
- trackClick({
1302
- pageName: PAGE_NAME,
1303
- buttonName: BUTTON_NAMES.BUSINESS,
1304
- data: formData,
1305
- });
1306
-
1307
- // Log the specific business data changes
1308
- console.group('Business Services Save Attempt');
1309
- console.log('Current Data:', beforeData);
1310
- console.log('Changes Being Applied:', businessChanges);
1311
- console.log('Final Form Data (partial):', formData);
1312
- console.log('Image Changes:', {
1313
- profileImage: uploadedImages.profileImage,
1314
- logoImage: uploadedImages.logoImage,
1315
- bannerImage: uploadedImages.bannerImage,
1316
- });
1317
- console.log('Services Selected:', selectedServices);
1318
- console.groupEnd();
1319
-
1320
- const result = await saveData(formData);
1321
- if (result.success) {
1322
- formHasUnsavedChanges[FORM_SECTION_HANDLER_MAP.BUSINESS_SERVICES.section] = false;
1323
- _$w('#saveBusinessButton').disable();
1324
- }
1325
- handleSaveDataFeedback(_$w('#businessMessage'), result.message);
1326
- _$w('#businessNameText').text = formData.businessName || DEFAULT_BUSINESS_NAME_TEXT;
1327
- }
1328
-
1329
- function setupTestimonials() {
1330
- const addTestimonialButton = _$w('#addTestimonialButton');
1331
-
1332
- addTestimonialButton.onClick(handleAddTestimonial);
1333
- _$w('#deleteTestimonialButton').onClick(async event => {
1334
- const data = _$w('#testimonialRepeater').data || [];
1335
- const clickedIndex = data.findIndex(item => item._id === event.context.itemId);
1336
- const result = await wixWindow.openLightbox(LIGHTBOX_NAMES.DELETE_CONFIRM);
1337
-
1338
- if (
1339
- result &&
1340
- result.toDelete &&
1341
- clickedIndex > 0 &&
1342
- Array.isArray(itemMemberObj.testimonial)
1343
- ) {
1344
- itemMemberObj.testimonial.splice(clickedIndex - 1, 1);
1345
- renderTestimonials();
1346
- checkFormChanges(FORM_SECTION_HANDLER_MAP.BUSINESS_SERVICES);
1347
- }
1348
- });
1349
-
1350
- renderTestimonials();
1351
- _$w('#testimonialRepeater').onItemReady(($item, itemData) => {
1352
- const msb = $item('#testimonialMSB');
1353
- if (itemData.isAdd) {
1354
- msb.changeState(TESTIMONIAL_STATES.ADD);
1355
- } else {
1356
- msb.changeState(TESTIMONIAL_STATES.VIEW);
1357
- $item('#testimonialText').text = itemData.text;
1358
- }
1359
- });
1360
- }
1361
-
1362
- function renderTestimonials() {
1363
- const testimonials =
1364
- itemMemberObj.testimonial === null
1365
- ? []
1366
- : Array.isArray(itemMemberObj.testimonial)
1367
- ? itemMemberObj.testimonial
1368
- : [];
1369
- const addItem = { _id: 'add-item', text: '', isAdd: true };
1370
- const testimonialData = [
1371
- addItem,
1372
- ...testimonials.map(text => ({ _id: generateId(), text, isAdd: false })),
1373
- ];
1374
-
1375
- setupRepeater('#testimonialRepeater', testimonialData);
1376
- }
1377
-
1378
- function handleAddTestimonial(event) {
1379
- const $clickedItem = _$w.at(event.context);
1380
- const input = $clickedItem('#testimonialsInput');
1381
- const newText = input.value;
1382
-
1383
- if (newText?.trim()) {
1384
- // Initialize testimonial array if it doesn't exist
1385
- if (!itemMemberObj.testimonial) {
1386
- itemMemberObj.testimonial = [];
1387
- }
1388
- itemMemberObj.testimonial.push(newText.trim());
1389
- input.value = '';
1390
- renderTestimonials();
1391
- checkFormChanges(FORM_SECTION_HANDLER_MAP.BUSINESS_SERVICES);
1392
- }
1393
- }
1394
-
1395
- function setContactBooking(fullProfilePageLink) {
1396
- // derive booleans only once
1397
- const showWixUrlCheckbox = !itemMemberObj.showWebsite && itemMemberObj.showWixUrl;
1398
- const showExistingUrlCheckbox = itemMemberObj.showWebsite;
1399
- _$w('#contactFormEmailInput').required = true;
1400
- // basic fields
1401
- _$w('#showCotactFormCheckbox').checked = itemMemberObj.showContactForm;
1402
- _$w('#contactFormEmailInput').value = itemMemberObj.contactFormEmail;
1403
- _$w('#schedulingLinkInput').value = itemMemberObj.bookingUrl;
1404
-
1405
- // URL part
1406
- _$w('#UrlInput').value = itemMemberObj.website || '';
1407
- _$w('#showUrlWixCheckbox').checked = showWixUrlCheckbox;
1408
- _$w('#showExsistingUrlCheckbox').checked = showExistingUrlCheckbox;
1409
- _$w('#urlWebsiteText').text = fullProfilePageLink;
1410
-
1411
- // custom validation for url inputs and email
1412
- handleOnCustomValidation(_$w('#UrlInput'));
1413
- handleOnCustomValidation(_$w('#schedulingLinkInput'));
1414
-
1415
- // enable/disable & styling in one pass
1416
- if (showWixUrlCheckbox) {
1417
- _$w('#UrlInput').disable();
1418
- _$w('#urlWebsiteText').customClassList.add('highlighted-text');
1419
- } else if (showExistingUrlCheckbox) {
1420
- _$w('#UrlInput').enable();
1421
- _$w('#urlWebsiteText').customClassList.remove('highlighted-text');
1422
- } else {
1423
- // neither checked: disable input & remove highlight
1424
- _$w('#UrlInput').disable();
1425
- _$w('#urlWebsiteText').customClassList.remove('highlighted-text');
1426
- }
1427
- syncOtherWebsiteUrlValidity();
1428
-
1429
- // clear buttons
1430
- _$w('#clearSchedulingLinkInput').onClick(() => {
1431
- _$w('#schedulingLinkInput').value = '';
1432
- checkFormChanges(FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING);
1433
- });
1434
- _$w('#clearExistingUrlLinkInput').onClick(() => {
1435
- _$w('#UrlInput').value = '';
1436
- checkFormChanges(FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING);
1437
- });
1438
-
1439
- // toggle handlers
1440
- _$w('#showUrlWixCheckbox').onChange(e => {
1441
- if (e.target.checked) {
1442
- _$w('#showExsistingUrlCheckbox').checked = false;
1443
- _$w('#UrlInput').disable();
1444
- _$w('#urlWebsiteText').customClassList.add('highlighted-text');
1445
- }
1446
- checkFormChanges(FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING);
1447
- });
1448
- _$w('#showExsistingUrlCheckbox').onChange(e => {
1449
- if (e.target.checked) {
1450
- _$w('#showUrlWixCheckbox').checked = false;
1451
- _$w('#UrlInput').enable();
1452
- _$w('#urlWebsiteText').customClassList.remove('highlighted-text');
1453
- }
1454
- checkFormChanges(FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING);
1455
- });
1456
-
1457
- setupAddressRepeater();
1458
- setupPhoneRepeater();
1459
- _$w('#saveContactBookingButton').onClick(saveContactBooking);
1460
- }
1461
-
1462
- /**
1463
- * Converts our internal address format to AddressInput component format
1464
- */
1465
- function convertToAddressInputFormat(address) {
1466
- if (!address) return null;
1467
-
1468
- if (!address.line1 && !address.city && !address.state && !address.postalcode) {
1469
- return null;
1470
- }
1471
-
1472
- const formatted = formatFullAddress(address);
1473
-
1474
- const hasValidCoordinates = Boolean(address.latitude && address.longitude);
1475
- const location = hasValidCoordinates
1476
- ? { latitude: address.latitude, longitude: address.longitude }
1477
- : null;
1478
-
1479
- return {
1480
- formatted,
1481
- location,
1482
- streetAddress: {
1483
- name: extractStreetName(address.line1),
1484
- number: extractStreetNumber(address.line1),
1485
- apt: address.line2 || '',
1486
- },
1487
- city: address.city || '',
1488
- subdivision: address.state || '',
1489
- country: address.country || 'US',
1490
- postalCode: address.postalcode || '',
1491
- };
1492
- }
1493
-
1494
- /**
1495
- * Converts AddressInput format back to our internal address format
1496
- */
1497
- function parseAddressInput(addressInputValue, existingAddress = null) {
1498
- if (!addressInputValue) return null;
1499
-
1500
- let line1 = '';
1501
- let line2 = '';
1502
-
1503
- if (addressInputValue.streetAddress) {
1504
- const number = addressInputValue.streetAddress.number || '';
1505
- const name = addressInputValue.streetAddress.name || '';
1506
- line1 = `${number} ${name}`.trim();
1507
-
1508
- // Capture apartment/suite/building info from streetAddress.apt (undocumented but exists)
1509
- if (addressInputValue.streetAddress.apt) {
1510
- line2 = addressInputValue.streetAddress.apt;
1511
- }
1512
- }
1513
-
1514
- if (!line1 && addressInputValue.formatted) {
1515
- line1 = addressInputValue.formatted.split(',')[0]?.trim() || '';
1516
- }
1517
-
1518
- // If line2 is still empty, try to extract building/suite info from formatted address
1519
- if (!line2 && addressInputValue.formatted) {
1520
- const formattedParts = addressInputValue.formatted.split(',').map(part => part.trim());
1521
- // Look for BLDG/STE/APT/UNIT/SUITE info in the formatted parts
1522
- for (let i = 1; i < formattedParts.length; i++) {
1523
- const part = formattedParts[i];
1524
- const lowerPart = part.toLowerCase();
1525
- if (
1526
- lowerPart.includes('bldg') ||
1527
- lowerPart.includes('ste') ||
1528
- lowerPart.includes('apt') ||
1529
- lowerPart.includes('unit') ||
1530
- lowerPart.includes('suite') ||
1531
- lowerPart.includes('#') ||
1532
- lowerPart.includes('building')
1533
- ) {
1534
- line2 = part;
1535
- break;
1536
- }
1537
- }
1538
- }
1539
-
1540
- return {
1541
- key: existingAddress?.key || generateId(),
1542
- line1,
1543
- line2: line2 || existingAddress?.line2 || '',
1544
- city: addressInputValue.city || '',
1545
- state: addressInputValue.subdivision || '',
1546
- postalcode: addressInputValue.postalCode || '',
1547
- country: addressInputValue.country || 'US',
1548
- latitude: addressInputValue.location?.latitude || existingAddress?.latitude || 0,
1549
- longitude: addressInputValue.location?.longitude || existingAddress?.longitude || 0,
1550
- addressStatus: existingAddress?.addressStatus || FALLBACK_ADDRESS_STATUS,
1551
- };
1552
- }
1553
-
1554
- function setupAddressRepeater() {
1555
- _$w('#addressesList').onItemReady(($item, itemData, index) =>
1556
- handleAddressItem($item, itemData, index)
1557
- );
1558
- renderAddressesList();
1559
-
1560
- _$w('#newAddressButton').onClick(addNewAddress);
1561
-
1562
- setupAddressRepeaterEventListeners();
1563
- }
1564
-
1565
- function setupAddressRepeaterEventListeners() {
1566
- _$w('#mainAddressCheckbox').onChange(async event => {
1567
- const data = _$w('#addressesList').data;
1568
- const clickedItemData = data.find(item => item._id === event.context.itemId);
1569
- const $item = _$w.at(event.context);
1570
- const checkbox = $item('#mainAddressCheckbox');
1571
- const isBeingUnchecked = !event.target.checked;
1572
-
1573
- if (isBeingUnchecked) {
1574
- const addressDisplayOptions = itemMemberObj.addressDisplayOption || [];
1575
- const mainAddressOption = addressDisplayOptions.find(opt => opt.isMain === true);
1576
-
1577
- // If this address is the main address, prevent unchecking
1578
- if (mainAddressOption && mainAddressOption.key === clickedItemData._id) {
1579
- checkbox.checked = true;
1580
- await wixWindow.openLightbox(LIGHTBOX_NAMES.MAIN_ADDRESS_ERROR);
1581
- return;
1582
- }
1583
- }
1584
-
1585
- _$w('#mainAddressCheckbox').checked = false;
1586
- checkbox.checked = true;
1587
-
1588
- // Primary address cannot be hidden: update model and repeater data so UI and save stay in sync
1589
- if (clickedItemData.address.addressStatus === ADDRESS_STATUS_TYPES.DONT_SHOW) {
1590
- updateAddressStatus(clickedItemData._id, ADDRESS_STATUS_TYPES.STATE_CITY_ZIP);
1591
- $item('#addressStatusOptions').value = ADDRESS_STATUS_TYPES.STATE_CITY_ZIP;
1592
- const currentData = _$w('#addressesList').data || [];
1593
- const dataWithStatusFixed = currentData.map(item =>
1594
- item._id === clickedItemData._id
1595
- ? {
1596
- ...item,
1597
- address: {
1598
- ...item.address,
1599
- addressStatus: ADDRESS_STATUS_TYPES.STATE_CITY_ZIP,
1600
- },
1601
- }
1602
- : item
1603
- );
1604
- _$w('#addressesList').data = dataWithStatusFixed;
1605
- }
1606
-
1607
- updateMainAddressSelection(clickedItemData._id);
1608
- refreshAddressListIsMainState();
1609
- checkFormChanges(FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING);
1610
- });
1611
-
1612
- _$w('#addressStatusOptions').onChange(event => {
1613
- const data = _$w('#addressesList').data || [];
1614
- const clickedItemData = data.find(item => item._id === event.context.itemId);
1615
- const newStatus = event.target.value;
1616
- const $item = _$w.at(event.context);
1617
- const isMain = $item('#mainAddressCheckbox').checked;
1618
-
1619
- if (isMain && newStatus === ADDRESS_STATUS_TYPES.DONT_SHOW) {
1620
- $item('#addressStatusOptions').value = ADDRESS_STATUS_TYPES.STATE_CITY_ZIP;
1621
- return;
1622
- }
1623
-
1624
- updateAddressStatus(clickedItemData._id, newStatus);
1625
-
1626
- // Keep repeater data in sync (required for new addresses not yet in itemMemberObj.addresses)
1627
- const updatedData = data.map(item =>
1628
- item._id === clickedItemData._id
1629
- ? { ...item, address: { ...item.address, addressStatus: newStatus } }
1630
- : item
1631
- );
1632
- _$w('#addressesList').data = updatedData;
1633
-
1634
- checkFormChanges(FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING);
1635
- });
1636
-
1637
- _$w('#addressItemEditBtn').onClick(event => {
1638
- const data = _$w('#addressesList').data;
1639
- const clickedItemData = data.find(item => item._id === event.context.itemId);
1640
- const $item = _$w.at(event.context);
1641
- $item('#addressItemStates').changeState(ADDRESS_STATES.EDIT);
1642
- const addressInputValue = convertToAddressInputFormat(clickedItemData.address);
1643
- $item('#addressEditInput').value = addressInputValue;
1644
- });
1645
-
1646
- _$w('#addressItemRemoveBtn').onClick(async event => {
1647
- const data = _$w('#addressesList').data || [];
1648
- const clickedItemData = data.find(item => item._id === event.context.itemId);
1649
- if (data.length <= 1 || clickedItemData?.isMain) {
1650
- return; // At least one address required; primary address cannot be deleted
1651
- }
1652
- const result = await wixWindow.openLightbox(LIGHTBOX_NAMES.DELETE_CONFIRM);
1653
- if (result && result.toDelete) {
1654
- removeAddress(clickedItemData._id);
1655
- }
1656
- });
1657
-
1658
- _$w('#addressEditInput').onChange(event => {
1659
- const $item = _$w.at(event.context);
1660
- validateAddressCompleteness($item);
1661
- });
1662
-
1663
- _$w('#addressEditCancelBtn').onClick(event => {
1664
- const data = _$w('#addressesList').data;
1665
- const clickedItemData = data.find(item => item._id === event.context.itemId);
1666
- const $item = _$w.at(event.context);
1667
- if (clickedItemData.isNewAddress) {
1668
- removeNewAddressFromRepeater(clickedItemData._id);
1669
- } else {
1670
- $item('#addressItemStates').changeState(ADDRESS_STATES.VIEW);
1671
- $item('#addressValidationMessage').hide();
1672
- }
1673
- });
1674
-
1675
- _$w('#addressEditSaveBtn').onClick(event => {
1676
- const data = _$w('#addressesList').data;
1677
- const clickedItemData = data.find(item => item._id === event.context.itemId);
1678
- const $item = _$w.at(event.context);
1679
- saveAddressFromSingleInput($item, clickedItemData);
1680
- });
1681
- }
1682
-
1683
- function addNewAddress() {
1684
- const currentData = _$w('#addressesList').data || [];
1685
-
1686
- if (currentData.length >= MAX_ADDRESSES_COUNT) {
1687
- return;
1688
- }
1689
-
1690
- const newAddressId = generateId();
1691
-
1692
- const newAddress = {
1693
- key: newAddressId,
1694
- line1: '',
1695
- line2: '',
1696
- city: '',
1697
- state: '',
1698
- postalcode: '',
1699
- country: 'US',
1700
- latitude: 0,
1701
- longitude: 0,
1702
- addressStatus: ADDRESS_STATUS_TYPES.STATE_CITY_ZIP,
1703
- };
1704
-
1705
- const isFirstAddress = currentData.length === 0;
1706
- const newAddressItem = {
1707
- _id: newAddressId,
1708
- address: newAddress,
1709
- isMain: isFirstAddress,
1710
- addressStatus: ADDRESS_STATUS_TYPES.STATE_CITY_ZIP,
1711
- isNewAddress: true,
1712
- };
1713
-
1714
- if (isFirstAddress) {
1715
- updateMainAddressSelection(newAddressId);
1716
- }
1717
-
1718
- renderAddressesList([...currentData, newAddressItem]);
1719
- }
1720
-
1721
- function handleAddressItem($item, itemData, index) {
1722
- const multiStateBox = $item('#addressItemStates');
1723
-
1724
- setupAddressViewState($item, itemData, index);
1725
- setupAddressEditState($item, itemData, index);
1726
-
1727
- if (itemData.isNewAddress) {
1728
- multiStateBox.changeState(ADDRESS_STATES.EDIT);
1729
- } else {
1730
- multiStateBox.changeState(ADDRESS_STATES.VIEW);
1731
- }
1732
- }
1733
-
1734
- function setupAddressViewState($item, itemData, index) {
1735
- const formattedAddress = formatFullAddress(itemData.address);
1736
-
1737
- $item('#addressItemtext').text = formattedAddress;
1738
- $item('#addressItemNumber').text = `Location ${index + 1}`;
1739
- $item('#mainAddressCheckbox').checked = itemData.isMain || false;
1740
-
1741
- const addressStatus = itemData.address.addressStatus || ADDRESS_STATUS_TYPES.STATE_CITY_ZIP;
1742
- $item('#addressStatusOptions').value = addressStatus;
1743
-
1744
- const addressCount = (_$w('#addressesList').data || []).length;
1745
- const canDelete = addressCount > 1 && !itemData.isMain;
1746
- if (canDelete) {
1747
- $item('#addressItemRemoveBtn').enable();
1748
- } else {
1749
- $item('#addressItemRemoveBtn').disable();
1750
- }
1751
- }
1752
-
1753
- function setupAddressEditState($item, itemData, _index) {
1754
- $item('#addressEditInput').enable();
1755
-
1756
- const addressInputValue = convertToAddressInputFormat(itemData.address);
1757
- $item('#addressEditInput').value = addressInputValue;
1758
-
1759
- $item('#addressValidationMessage').hide();
1760
- }
1761
-
1762
- function validateAddressCompleteness($item) {
1763
- const addressInput = $item('#addressEditInput');
1764
- const saveBtn = $item('#addressEditSaveBtn');
1765
- const validationMessage = $item('#addressValidationMessage');
1766
-
1767
- const addressValue = addressInput.value;
1768
-
1769
- if (!addressValue) {
1770
- showAddressValidationError(validationMessage, saveBtn, 'Please provide a complete address');
1771
- return false;
1772
- }
1773
-
1774
- const missingFields = [];
1775
-
1776
- if (!addressValue.streetAddress?.name) {
1777
- missingFields.push('street name');
1778
- }
1779
-
1780
- if (!addressValue.streetAddress?.number) {
1781
- missingFields.push('street number');
1782
- }
1783
-
1784
- if (!addressValue.city) {
1785
- missingFields.push('city');
1786
- }
1787
-
1788
- if (!addressValue.subdivision) {
1789
- missingFields.push('state');
1790
- }
1791
-
1792
- if (!addressValue.postalCode) {
1793
- missingFields.push('postal code');
1794
- }
1795
-
1796
- if (!addressValue.location?.latitude || !addressValue.location?.longitude) {
1797
- missingFields.push('valid location details');
1798
- }
1799
-
1800
- if (missingFields.length > 0) {
1801
- const message = `Please provide: ${missingFields.join(', ')}`;
1802
- showAddressValidationError(validationMessage, saveBtn, message);
1803
- return false;
1804
- }
1805
-
1806
- hideAddressValidationError(validationMessage, saveBtn);
1807
- return true;
1808
- }
1809
-
1810
- function showAddressValidationError(validationMessage, saveBtn, message) {
1811
- validationMessage.text = message;
1812
- validationMessage.show();
1813
- saveBtn.disable();
1814
- }
1815
-
1816
- function hideAddressValidationError(validationMessage, saveBtn) {
1817
- validationMessage.hide();
1818
- saveBtn.enable();
1819
- }
1820
-
1821
- function saveAddressFromSingleInput($item, itemData) {
1822
- if (!validateAddressCompleteness($item)) {
1823
- return;
1824
- }
1825
-
1826
- const addressInput = $item('#addressEditInput');
1827
- const addressValue = addressInput.value;
1828
-
1829
- const convertedAddress = parseAddressInput(addressValue, itemData.address);
1830
-
1831
- const formattedAddress = formatFullAddress(convertedAddress);
1832
- $item('#addressItemtext').text = formattedAddress;
1833
-
1834
- if (itemData.isNewAddress) {
1835
- setNewAddress(itemData._id, convertedAddress);
1836
- } else {
1837
- updateAddress(itemData._id, convertedAddress);
1838
- }
1839
-
1840
- $item('#addressItemStates').changeState(ADDRESS_STATES.VIEW);
1841
- $item('#addressValidationMessage').hide();
1842
- }
1843
-
1844
- function removeNewAddressFromRepeater(addressId) {
1845
- const currentData = _$w('#addressesList').data || [];
1846
- const updatedData = currentData.filter(item => item._id !== addressId);
1847
- _$w('#addressesList').data = updatedData;
1848
- }
1849
-
1850
- function setNewAddress(addressId, addressData) {
1851
- if (!itemMemberObj.addresses) {
1852
- itemMemberObj.addresses = [];
1853
- }
1854
- const isFirstAddress = itemMemberObj.addresses.length === 0;
1855
- itemMemberObj.addresses.push(addressData);
1856
-
1857
- if (!itemMemberObj.addressDisplayOption) {
1858
- itemMemberObj.addressDisplayOption = [];
1859
- }
1860
- const existingOption = itemMemberObj.addressDisplayOption.find(
1861
- opt => opt.key === (addressData.key || addressId)
1862
- );
1863
- if (!existingOption) {
1864
- itemMemberObj.addressDisplayOption.push({
1865
- key: addressData.key || addressId,
1866
- isMain: isFirstAddress,
1867
- });
1868
- }
1869
-
1870
- checkFormChanges(FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING);
1871
- }
1872
-
1873
- function extractStreetNumber(line1) {
1874
- if (!line1) return '';
1875
- const match = line1.match(/^\d+/);
1876
- return match ? match[0] : '';
1877
- }
1878
-
1879
- function extractStreetName(line1) {
1880
- if (!line1) return '';
1881
- return line1.replace(/^\d+\s*/, '').trim();
1882
- }
1883
-
1884
- function renderAddressesList(updatedAddresses) {
1885
- let addressData = updatedAddresses || [];
1886
-
1887
- if (!addressData || addressData?.length === 0) {
1888
- const addresses = Array.isArray(itemMemberObj.addresses) ? itemMemberObj.addresses : [];
1889
- const displayOptions = Array.isArray(itemMemberObj.addressDisplayOption)
1890
- ? itemMemberObj.addressDisplayOption
1891
- : [];
1892
-
1893
- addressData = addresses.map((address, index) => {
1894
- const displayOption = displayOptions.find(opt => opt.key === address.key);
1895
- return {
1896
- _id: address.key || `address_${index}`,
1897
- address,
1898
- isMain: displayOption?.isMain || false,
1899
- addressStatus: address.addressStatus || FALLBACK_ADDRESS_STATUS,
1900
- isNewAddress: false,
1901
- };
1902
- });
1903
- }
1904
-
1905
- const repeater = _$w('#addressesList');
1906
- repeater.data = addressData;
1907
-
1908
- updateAddressAddButtonState();
1909
- }
1910
-
1911
- function updateAddressAddButtonState() {
1912
- const currentData = _$w('#addressesList').data || [];
1913
- const newAddressButton = _$w('#newAddressButton');
1914
-
1915
- if (currentData.length >= MAX_ADDRESSES_COUNT) {
1916
- newAddressButton.disable();
1917
- } else {
1918
- newAddressButton.enable();
1919
- }
1920
- }
1921
-
1922
- function updateMainAddressSelection(selectedId) {
1923
- if (!itemMemberObj.addressDisplayOption) {
1924
- itemMemberObj.addressDisplayOption = [];
1925
- }
1926
-
1927
- itemMemberObj.addressDisplayOption.forEach(option => {
1928
- option.isMain = false;
1929
- });
1930
-
1931
- let selectedOption = itemMemberObj.addressDisplayOption.find(opt => opt.key === selectedId);
1932
-
1933
- // If the option doesn't exist, create it
1934
- if (!selectedOption) {
1935
- selectedOption = { key: selectedId, isMain: false };
1936
- itemMemberObj.addressDisplayOption.push(selectedOption);
1937
- }
1938
-
1939
- selectedOption.isMain = true;
1940
- }
1941
-
1942
- /**
1943
- * Refreshes each repeater item's isMain from itemMemberObj.addressDisplayOption,
1944
- * updates repeater data, and explicitly updates each item's delete button state
1945
- * (repeater may not re-run onItemReady when only isMain changes).
1946
- * Uses forEachItem because Wix Repeater does not have .at(index).
1947
- */
1948
- function refreshAddressListIsMainState() {
1949
- const repeater = _$w('#addressesList');
1950
- const currentData = repeater.data || [];
1951
- const displayOptions = itemMemberObj.addressDisplayOption || [];
1952
- const updatedData = currentData.map(item => {
1953
- const opt = displayOptions.find(o => o.key === item._id);
1954
- return { ...item, isMain: opt?.isMain || false };
1955
- });
1956
- repeater.data = updatedData;
1957
-
1958
- const addressCount = updatedData.length;
1959
- repeater.forEachItem(($item, _itemData, index) => {
1960
- const isMain = index < updatedData.length ? updatedData[index].isMain : false;
1961
- const canDelete = addressCount > 1 && !isMain;
1962
- if (canDelete) {
1963
- $item('#addressItemRemoveBtn').enable();
1964
- } else {
1965
- $item('#addressItemRemoveBtn').disable();
1966
- }
1967
- });
1968
- }
1969
-
1970
- function updateAddressStatus(addressId, newStatus) {
1971
- const addresses = Array.isArray(itemMemberObj.addresses) ? itemMemberObj.addresses : [];
1972
- const addressIndex = addresses.findIndex(
1973
- addr => (addr.key || `address_${addresses.indexOf(addr)}`) === addressId
1974
- );
1975
-
1976
- if (addressIndex !== -1) {
1977
- itemMemberObj.addresses[addressIndex].addressStatus = newStatus;
1978
- }
1979
- }
1980
-
1981
- function updateAddress(addressId, newAddressValue) {
1982
- const addresses = Array.isArray(itemMemberObj.addresses) ? itemMemberObj.addresses : [];
1983
- const addressIndex = addresses.findIndex(
1984
- addr => (addr.key || `address_${addresses.indexOf(addr)}`) === addressId
1985
- );
1986
-
1987
- if (addressIndex !== -1) {
1988
- itemMemberObj.addresses[addressIndex] = {
1989
- ...itemMemberObj.addresses[addressIndex],
1990
- ...newAddressValue,
1991
- };
1992
-
1993
- checkFormChanges(FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING);
1994
- }
1995
- }
1996
-
1997
- function removeAddress(addressId) {
1998
- const currentData = _$w('#addressesList').data || [];
1999
- const itemToRemove = currentData.find(item => item._id === addressId);
2000
- if (currentData.length <= 1 || itemToRemove?.isMain) {
2001
- return; // At least one address required; primary address cannot be deleted
2002
- }
2003
-
2004
- if (itemMemberObj.addresses) {
2005
- itemMemberObj.addresses = itemMemberObj.addresses.filter(
2006
- addr => (addr.key || `address_${itemMemberObj.addresses.indexOf(addr)}`) !== addressId
2007
- );
2008
- }
2009
-
2010
- if (itemMemberObj.addressDisplayOption) {
2011
- itemMemberObj.addressDisplayOption = itemMemberObj.addressDisplayOption.filter(
2012
- opt => opt.key !== addressId
2013
- );
2014
- }
2015
-
2016
- renderAddressesList();
2017
- checkFormChanges(FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING);
2018
- }
2019
-
2020
- function formatFullAddress(addr) {
2021
- if (!addr) return '';
2022
-
2023
- const parts = [];
2024
-
2025
- if (addr.line1) parts.push(addr.line1);
2026
- if (addr.line2) parts.push(addr.line2); // Include building/suite info
2027
- if (addr.city) parts.push(addr.city);
2028
- if (addr.state && addr.postalcode) {
2029
- parts.push(`${addr.state} ${addr.postalcode}`);
2030
- } else if (addr.state) {
2031
- parts.push(addr.state);
2032
- } else if (addr.postalcode) {
2033
- parts.push(addr.postalcode);
2034
- }
2035
-
2036
- return parts.join(', ') || 'No address entered';
2037
- }
2038
-
2039
- function setupPhoneRepeater() {
2040
- _$w('#phoneNumbersList').onItemReady(handlePhoneItem);
2041
- renderPhonesList();
2042
-
2043
- _$w('#addPhoneButton').onClick(addNewPhone);
2044
-
2045
- setupPhoneRepeaterEventListeners();
2046
- }
2047
-
2048
- function setupPhoneRepeaterEventListeners() {
2049
- _$w('#phoneInput').onInput(event => {
2050
- const data = _$w('#phoneNumbersList').data;
2051
- const clickedItemData = data.find(item => item._id === event.context.itemId);
2052
- const digits = digitsFromPhoneInput(event.target.value);
2053
- const formatted = formatUsPhoneInput(digits);
2054
- if (event.target.value !== formatted) {
2055
- event.target.value = formatted;
2056
- }
2057
-
2058
- updatePhoneNumber(clickedItemData._id, formatted);
2059
-
2060
- if (clickedItemData.isNewPhone && digits.length > 0) {
2061
- addNewPhoneToData(clickedItemData._id, formatted);
2062
- clickedItemData.isNewPhone = false;
2063
- }
2064
-
2065
- checkFormChanges(FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING);
2066
- });
2067
-
2068
- _$w('#showPhoneCheckbox').onChange(event => {
2069
- const data = _$w('#phoneNumbersList').data || [];
2070
- const clickedItemData = data.find(item => item._id === event.context.itemId);
2071
- if (!clickedItemData) {
2072
- return;
2073
- }
2074
-
2075
- const $item = _$w.at(event.context);
2076
- const isChecked = event.target.checked;
2077
- let updated;
2078
- if (isChecked) {
2079
- // Same pattern as #mainAddressCheckbox: clear every row, then select one (repeater
2080
- // often does not refresh sibling items' controls when only `data` changes).
2081
- _$w('#showPhoneCheckbox').checked = false;
2082
- $item('#showPhoneCheckbox').checked = true;
2083
- updated = data.map(item =>
2084
- item._id === clickedItemData._id
2085
- ? { ...item, showPhone: true }
2086
- : { ...item, showPhone: false }
2087
- );
2088
- updateShowPhoneSelection(clickedItemData._id, true);
2089
- } else {
2090
- updated = data.map(item =>
2091
- item._id === clickedItemData._id ? { ...item, showPhone: false } : item
2092
- );
2093
- updateShowPhoneSelection(clickedItemData._id, false);
2094
- }
2095
-
2096
- renderPhonesList(updated);
2097
- checkFormChanges(FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING);
2098
- });
2099
-
2100
- _$w('#removePhoneBtn').onClick(async event => {
2101
- const data = _$w('#phoneNumbersList').data;
2102
- const clickedItemData = data.find(item => item._id === event.context.itemId);
2103
- const result = await wixWindow.openLightbox(LIGHTBOX_NAMES.DELETE_CONFIRM);
2104
- if (result && result.toDelete) {
2105
- removePhone(clickedItemData._id);
2106
- }
2107
- });
2108
- }
2109
-
2110
- function addNewPhone() {
2111
- const currentData = _$w('#phoneNumbersList').data || [];
2112
-
2113
- if (currentData.length >= MAX_PHONES_COUNT) {
2114
- return;
2115
- }
2116
-
2117
- const newPhoneId = generateId();
2118
- const newPhoneItem = {
2119
- _id: newPhoneId,
2120
- phoneNumber: '',
2121
- showPhone: false,
2122
- isNewPhone: true,
2123
- phoneIndex: currentData.length + 1,
2124
- };
2125
-
2126
- renderPhonesList([...currentData, newPhoneItem]);
2127
- }
2128
-
2129
- function handlePhoneItem($item, itemData) {
2130
- $item('#phoneInput').value = formatPhoneNumberForInput(itemData.phoneNumber || '');
2131
- $item('#showPhoneCheckbox').checked = itemData.showPhone || false;
2132
- $item('#phoneNumberLabel').text = `Phone ${itemData.phoneIndex}`;
2133
- }
2134
-
2135
- /** Only one "Show your phone" row at a time; aligns UI with itemMemberObj.toShowPhone. */
2136
- function normalizePhoneShowFlags(phoneData) {
2137
- if (!phoneData?.length) return phoneData;
2138
-
2139
- const cms = (itemMemberObj.toShowPhone || '').trim();
2140
- if (cms) {
2141
- let matched = false;
2142
- return phoneData.map(p => {
2143
- const t = (p.phoneNumber || '').trim();
2144
- const isMatch = Boolean(t && phoneDigitsEqual(t, cms));
2145
- if (isMatch && !matched) {
2146
- matched = true;
2147
- return { ...p, showPhone: true };
2148
- }
2149
- return { ...p, showPhone: false };
2150
- });
2151
- }
2152
-
2153
- const trueIdxs = phoneData.map((p, i) => (p.showPhone ? i : -1)).filter(i => i >= 0);
2154
- if (trueIdxs.length <= 1) return phoneData;
2155
-
2156
- const keep = trueIdxs[0];
2157
- return phoneData.map((p, i) => ({
2158
- ...p,
2159
- showPhone: i === keep,
2160
- }));
2161
- }
2162
-
2163
- function renderPhonesList(updatedPhones) {
2164
- let phoneData = updatedPhones || [];
2165
-
2166
- if (!phoneData || phoneData?.length === 0) {
2167
- const phones = Array.isArray(itemMemberObj.phones) ? itemMemberObj.phones : [];
2168
-
2169
- phoneData = phones.map((phone, index) => ({
2170
- _id: `phone_${index}`,
2171
- phoneNumber: phone,
2172
- showPhone: phoneDigitsEqual(phone, itemMemberObj.toShowPhone),
2173
- isNewPhone: false,
2174
- phoneIndex: index + 1,
2175
- }));
2176
- }
2177
-
2178
- phoneData = normalizePhoneShowFlags(phoneData);
2179
-
2180
- const repeater = _$w('#phoneNumbersList');
2181
-
2182
- repeater.data = phoneData;
2183
- refreshPhoneShowCheckboxState(phoneData);
2184
- updatePhoneAddButtonState();
2185
- }
2186
-
2187
- /**
2188
- * Repeater may not re-run onItemReady for sibling items when only showPhone flags change;
2189
- * keep each #showPhoneCheckbox in sync with data (same idea as refreshAddressListIsMainState).
2190
- */
2191
- function refreshPhoneShowCheckboxState(phoneData) {
2192
- const repeater = _$w('#phoneNumbersList');
2193
- const data = phoneData || repeater.data || [];
2194
- repeater.forEachItem(($item, _itemData, index) => {
2195
- const show = index < data.length ? data[index].showPhone : false;
2196
- $item('#showPhoneCheckbox').checked = Boolean(show);
2197
- });
2198
- }
2199
-
2200
- function updatePhoneAddButtonState() {
2201
- const currentData = _$w('#phoneNumbersList').data || [];
2202
- const addPhoneButton = _$w('#addPhoneButton');
2203
-
2204
- if (currentData.length >= MAX_PHONES_COUNT) {
2205
- addPhoneButton.disable();
2206
- } else {
2207
- addPhoneButton.enable();
2208
- }
2209
- }
2210
-
2211
- function updatePhoneNumber(phoneId, newPhoneNumber) {
2212
- const currentData = _$w('#phoneNumbersList').data || [];
2213
- const itemIndex = currentData.findIndex(item => item._id === phoneId);
2214
-
2215
- if (itemIndex !== -1) {
2216
- const item = currentData[itemIndex];
2217
- const prevTrimmed = (item.phoneNumber || '').trim();
2218
- const newTrimmed = newPhoneNumber.trim();
2219
-
2220
- item.phoneNumber = newPhoneNumber;
2221
-
2222
- if (item.showPhone) {
2223
- itemMemberObj.toShowPhone = newTrimmed || null;
2224
- } else if (
2225
- itemMemberObj.toShowPhone &&
2226
- prevTrimmed &&
2227
- phoneDigitsEqual(itemMemberObj.toShowPhone, prevTrimmed)
2228
- ) {
2229
- itemMemberObj.toShowPhone = newTrimmed || null;
2230
- }
2231
-
2232
- renderPhonesList(currentData);
2233
- syncPhonesFromRepeater();
2234
- checkFormChanges(FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING);
2235
- }
2236
- }
2237
-
2238
- function syncPhonesFromRepeater() {
2239
- const phoneData = _$w('#phoneNumbersList').data || [];
2240
- itemMemberObj.phones = phoneData
2241
- .filter(item => !item.isNewPhone && item.phoneNumber.trim())
2242
- .map(item => item.phoneNumber);
2243
- }
2244
-
2245
- function addNewPhoneToData(phoneId, phoneNumber) {
2246
- if (!itemMemberObj.phones) {
2247
- itemMemberObj.phones = [];
2248
- }
2249
-
2250
- itemMemberObj.phones.push(phoneNumber);
2251
- renderPhonesList();
2252
- checkFormChanges(FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING);
2253
- }
2254
-
2255
- function removePhone(phoneId) {
2256
- const currentData = _$w('#phoneNumbersList').data || [];
2257
- const phoneToRemove = currentData.find(item => item._id === phoneId);
2258
-
2259
- if (phoneToRemove) {
2260
- if (itemMemberObj.phones) {
2261
- itemMemberObj.phones = itemMemberObj.phones.filter(
2262
- phone => !phoneDigitsEqual(phone, phoneToRemove.phoneNumber)
2263
- );
2264
- }
2265
-
2266
- // Clear toShowPhone if it was the removed phone or if it's no longer in the list
2267
- // (handles format mismatch e.g. "(406)655-4940" vs "(406) 655-4940")
2268
- const remainingPhones = Array.isArray(itemMemberObj.phones) ? itemMemberObj.phones : [];
2269
- if (
2270
- itemMemberObj.toShowPhone &&
2271
- !remainingPhones.some(p => phoneDigitsEqual(p, itemMemberObj.toShowPhone))
2272
- ) {
2273
- itemMemberObj.toShowPhone = null;
2274
- }
2275
-
2276
- const updatedData = currentData.filter(item => item._id !== phoneId);
2277
- renderPhonesList(updatedData);
2278
- checkFormChanges(FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING);
2279
- }
2280
- }
2281
-
2282
- function updateShowPhoneSelection(phoneId, isVisible) {
2283
- const currentData = _$w('#phoneNumbersList').data || [];
2284
- const selectedItem = currentData.find(item => item._id === phoneId);
2285
-
2286
- if (!selectedItem) {
2287
- return;
2288
- }
2289
-
2290
- const trimmed = (selectedItem.phoneNumber || '').trim();
2291
-
2292
- if (isVisible) {
2293
- itemMemberObj.toShowPhone = trimmed || null;
2294
- return;
2295
- }
2296
-
2297
- if (
2298
- itemMemberObj.toShowPhone === trimmed ||
2299
- phoneDigitsEqual(itemMemberObj.toShowPhone, trimmed)
2300
- ) {
2301
- itemMemberObj.toShowPhone = null;
2302
- }
2303
- }
2304
-
2305
- function getToShowPhone() {
2306
- const phones = Array.isArray(itemMemberObj.phones) ? itemMemberObj.phones : [];
2307
- const toShow = itemMemberObj.toShowPhone || null;
2308
- // Never expose toShowPhone when phones is empty, so save payload clears it in CMS
2309
- if (phones.length === 0) return null;
2310
- // If toShowPhone is not in the list (e.g. format mismatch), treat as none selected
2311
- if (toShow && !phones.some(p => phoneDigitsEqual(p, toShow))) return null;
2312
- return toShow;
2313
- }
2314
-
2315
- function getContactAndBookingData() {
2316
- const showWixUrl = _$w('#showUrlWixCheckbox').checked;
2317
- const showExistingUrl = _$w('#showExsistingUrlCheckbox').checked;
2318
-
2319
- const addresses = Array.isArray(itemMemberObj.addresses) ? itemMemberObj.addresses : [];
2320
- const phones = Array.isArray(itemMemberObj.phones) ? itemMemberObj.phones : [];
2321
-
2322
- const rawWebsite = (_$w('#UrlInput').value || '').trim();
2323
- const rawBooking = (_$w('#schedulingLinkInput').value || '').trim();
2324
-
2325
- return {
2326
- showContactForm: _$w('#showCotactFormCheckbox').checked,
2327
- contactFormEmail: _$w('#contactFormEmailInput').value,
2328
- toShowPhone: getToShowPhone(),
2329
- bookingUrl: rawBooking ? normalizeExternalUrl(rawBooking) : '',
2330
- website: rawWebsite ? normalizeExternalUrl(rawWebsite) : '',
2331
- showWebsite: showExistingUrl,
2332
- showWixUrl,
2333
- addresses,
2334
- addressDisplayOption: itemMemberObj.addressDisplayOption || [],
2335
- phones,
2336
- };
2337
- }
2338
-
2339
- async function saveContactBooking() {
2340
- const showExistingUrl = _$w('#showExsistingUrlCheckbox').checked;
2341
- const otherWebsiteValue = (_$w('#UrlInput').value || '').trim();
2342
- const isOtherWebsiteInvalid =
2343
- showExistingUrl && (otherWebsiteValue === '' || isNotValidUrl(otherWebsiteValue));
2344
- if (isOtherWebsiteInvalid) {
2345
- const $urlInput = _$w('#UrlInput');
2346
- $urlInput.required = true;
2347
- $urlInput.updateValidityIndication();
2348
- return;
2349
- }
2350
-
2351
- const beforeData = JSON.parse(JSON.stringify(itemMemberObj));
2352
- const contactChanges = getContactAndBookingData();
2353
-
2354
- // Only send contact fields + required identifiers (partial update)
2355
- const formData = {
2356
- _id: itemMemberObj._id,
2357
- memberId: itemMemberObj.memberId,
2358
- ...contactChanges,
2359
- };
2360
-
2361
- trackClick({
2362
- pageName: PAGE_NAME,
2363
- buttonName: BUTTON_NAMES.CONTACT,
2364
- data: formData,
2365
- });
2366
-
2367
- // Log the specific contact & booking data changes
2368
- console.group('Contact & Booking Save Attempt');
2369
- console.log('Current Data:', beforeData);
2370
- console.log('Changes Being Applied:', contactChanges);
2371
- console.log('Final Form Data (partial):', formData);
2372
- console.log('Address Changes:', {
2373
- addressCount: contactChanges.addresses?.length || 0,
2374
- addressDisplayOptions: contactChanges.addressDisplayOption,
2375
- });
2376
- console.log('Phone Selection:', contactChanges.toShowPhone);
2377
- console.groupEnd();
2378
-
2379
- const result = await saveData(formData);
2380
- if (result.success) {
2381
- if (beforeData.showWebsite !== contactChanges.showWebsite) {
2382
- handleOptWebsiteCheckboxEnable(showExistingUrl);
2383
- }
2384
- // Sync Personal Details opt-in from saved member.
2385
- _$w('#optWebsiteCheckbox').checked = itemMemberObj.showWixUrl;
2386
- toggleFreeWebsiteText(itemMemberObj.showWixUrl);
2387
- formHasUnsavedChanges[FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING.section] = false;
2388
- _$w('#saveContactBookingButton').disable();
2389
- }
2390
- handleSaveDataFeedback(_$w('#contactMessage'), result.message);
2391
- }
2392
-
2393
- function handleSaveDataFeedback($messageElement, message) {
2394
- $messageElement.text = message;
2395
- $messageElement.expand();
2396
- setTimeout(() => {
2397
- $messageElement.collapse();
2398
- }, 5000);
2399
- }
2400
-
2401
- function setGallery() {
2402
- // Initialize gallery array if it doesn't exist
2403
- if (!itemMemberObj.gallery) {
2404
- itemMemberObj.gallery = [];
2405
- }
2406
- const gallery = itemMemberObj.gallery;
2407
-
2408
- const galleryData = buildGalleryData(gallery);
2409
-
2410
- _$w('#galleryRepeater').data = galleryData;
2411
- }
2412
-
2413
- function buildGalleryData(gallery) {
2414
- return [
2415
- { _id: 'add-item', isAdd: true },
2416
- ...gallery.map(image => ({
2417
- _id: generateId(),
2418
- image,
2419
- isAdd: false,
2420
- })),
2421
- ];
2422
- }
2423
-
2424
- function handleGalleryItem($item, itemData) {
2425
- const multiStateBox = $item('#galleryMSB');
2426
-
2427
- if (itemData.isAdd) {
2428
- setupAddImageState($item, multiStateBox);
2429
- } else {
2430
- setupImageState($item, itemData, multiStateBox);
2431
- }
2432
- }
2433
-
2434
- function setupAddImageState($item, multiStateBox) {
2435
- multiStateBox.changeState(GALLERY_STATES.ADD);
2436
- }
2437
-
2438
- function setupImageState($item, itemData, multiStateBox) {
2439
- multiStateBox.changeState(GALLERY_STATES.VIEW);
2440
- $item('#galleryImage').src = itemData.image.src;
2441
- }
2442
-
2443
- async function saveGalleryToCMS() {
2444
- // Only send gallery field + required identifiers (partial update)
2445
- const formData = {
2446
- _id: itemMemberObj._id,
2447
- memberId: itemMemberObj.memberId,
2448
- gallery: itemMemberObj.gallery,
2449
- };
2450
-
2451
- await saveData(formData);
2452
- }
2453
-
2454
- function formatFileName(fullName, maxBaseLength = 23) {
2455
- const dotIndex = fullName.lastIndexOf('.');
2456
- if (dotIndex === -1 || fullName.length <= maxBaseLength + 4) return fullName;
2457
-
2458
- const name = fullName.slice(0, dotIndex);
2459
- const ext = fullName.slice(dotIndex);
2460
- return `${name.slice(0, maxBaseLength)}...${ext}`;
2461
- }
2462
-
2463
- async function filterInterests(searchValue) {
2464
- const container = _$w('#containerRepeaterInterest');
2465
- const repeater = _$w('#repeaterInterest');
2466
-
2467
- const allInterests = await getInterestAll();
2468
- const filtered = allInterests
2469
- .filter(val => val.toLowerCase().includes(searchValue))
2470
- .map(val => ({ _id: generateId(), value: val }));
2471
-
2472
- if (filtered.length > 0) {
2473
- repeater.data = filtered;
2474
- container.expand();
2475
- } else {
2476
- repeater.data = [];
2477
- container.collapse();
2478
- }
2479
-
2480
- return filtered;
2481
- }
2482
- }
2483
-
2484
- module.exports = {
2485
- personalDetailsOnReady,
2486
- };