PageSourceSearch

https://saddleback.com/scripts/MyGroups/Archive/InfoTabRock.js?v=517cdeafa93af7720f65d0a6f449ca34186eeedc

js saddleback.com collected 2026-09-24 17:19:49 UTC 64,369 bytes, 1,494 lines download raw bytes

1import GroupImageModal from '/scripts/MyGroups/Modals/GroupImageModal.js';
2import {
3  DAYS_OPTIONS,
4  ROCK_AGES_OPTIONS,
5  ROCK_GROUP_TYPE_OPTIONS,
6  ROCK_GENDER_OPTIONS,
7  ROCK_LIFE_STAGE_OPTIONS,
8  ROCK_LANGUAGE_OPTIONS,
9  ROCK_MEETING_TYPE,
10  ROCK_MEETING_TYPE_OPTIONS,
11  GROUP_CAPACITY_OPTIONS,
12  ROCK_TIMEZONE_OPTIONS,
13} from '/scripts/MyGroups/constantsV2.js';
14
15export default {
16  name: 'InfoTab',
17
18  emits: ['refetch-group'],
19
20  props: [
21    'visible',
22    'isParticipant',
23    'campusList',
24    'originalGroupInfo',
25    'healthyChurchApi',
26    'healthyChurchSmallGroupsApi',
27    'authHeaders',
28    'isFetching',
29    'countryList',
30    'regionLabels',
31    'rockCampuses',
32    'rockDomain',
33    'spanishGroupTypeOptions',
34    'spanishLifeStageOptions',
35    'spanishMeetingTypeOptions',
36  ],
37
38  components: {
39    GroupImageModal,
40  },
41
42  template: `
43        <div class="my-groups-container" v-show="visible && !isParticipant">
44            <div class="group-photo-container">
45                <div class="group-photo">
46                    <div class="photo-image" v-if="groupInfo.profilePictureUrl">
47                        <svg class="camera-icon" v-on:click="showGroupImageModal(true)" width="38"
48                            height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg">
49                            <rect width="38" height="38" rx="19" fill="black" fill-opacity="0.48" />
50                            <path
51                                d="M15.2383 12.5352C15.4492 11.8672 16.082 11.375 16.8203 11.375H21.1445C21.8828 11.375 22.5156 11.8672 22.7266 12.5352L23.1133 13.625H25.75C26.9805 13.625 28 14.6445 28 15.875V24.875C28 26.1406 26.9805 27.125 25.75 27.125H12.25C10.9844 27.125 10 26.1406 10 24.875V15.875C10 14.6445 10.9844 13.625 12.25 13.625H14.8516L15.2383 12.5352ZM19 17C17.7695 17 16.6797 17.668 16.0469 18.6875C15.4492 19.7422 15.4492 21.043 16.0469 22.0625C16.6797 23.1172 17.7695 23.75 19 23.75C20.1953 23.75 21.2852 23.1172 21.918 22.0625C22.5156 21.043 22.5156 19.7422 21.918 18.6875C21.2852 17.668 20.1953 17 19 17Z"
52                                fill="white" />
53                        </svg>
54                        <img
55                            class="photo"
56                            :src="rockDomain + groupInfo.profilePictureUrl"
57                            alt=""
58                        />
59                    </div>
60                    <div class="photo-placeholder" v-if="!groupInfo.profilePictureUrl">
61                        <svg class="camera-icon" v-on:click="showGroupImageModal(true)" width="38"
62                            height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg">
63                            <rect width="38" height="38" rx="19" fill="black" fill-opacity="0.48" />
64                            <path
65                                d="M15.2383 12.5352C15.4492 11.8672 16.082 11.375 16.8203 11.375H21.1445C21.8828 11.375 22.5156 11.8672 22.7266 12.5352L23.1133 13.625H25.75C26.9805 13.625 28 14.6445 28 15.875V24.875C28 26.1406 26.9805 27.125 25.75 27.125H12.25C10.9844 27.125 10 26.1406 10 24.875V15.875C10 14.6445 10.9844 13.625 12.25 13.625H14.8516L15.2383 12.5352ZM19 17C17.7695 17 16.6797 17.668 16.0469 18.6875C15.4492 19.7422 15.4492 21.043 16.0469 22.0625C16.6797 23.1172 17.7695 23.75 19 23.75C20.1953 23.75 21.2852 23.1172 21.918 22.0625C22.5156 21.043 22.5156 19.7422 21.918 18.6875C21.2852 17.668 20.1953 17 19 17Z"
66                                fill="white" />
67                        </svg>
68                        <img
69                            :src="groupInfo.smallGroupTypeId ? 
70                                '/small-group-types-thumbnails/thumbnail-' + this.mapThumbnailIndex(groupInfo.smallGroupTypeId) + '.png' : 
71                                '/small-group-types-thumbnails/thumbnail-default.png'"
72                            class="svg-overlay"
73                            alt=""
74                        />
75                    </div>
76                    <p class="name-container">
77                    </p>
78                </div>
79                <div class="group-photo-spacer"></div>
80            </div>
81            <form id="myProfileUpdate" onsubmit="return false">
82                <div class="group-details-container">
83                    <div class="group-details-column">
84                        <!-- Group Name -->
85                        <div class="form-input">
86                            <label for="groupNamePrefix">
87                                Group Name
88                                <span class="required" v-if="!groupInfo.name">*</span>
89                            </label>
90                            <input type="text" id="groupNamePrefix" class="saddleback-input" v-model="groupInfo.name" :disabled="!canEdit"/>
91                        </div>
92
93                        <!-- Group Description -->
94                        <div class="text-box">
95                            <label for="description">
96                                Group Description
97                                <span class="required" v-if="!groupInfo.description">*</span>
98                            </label>
99                            <textarea id="description" rows="3" v-model="groupInfo.description" :disabled="!canEdit"/>
100                        </div>
101
102                        <!-- Meeting Day -->
103                        <div class="select-container">
104                            <div class="select-item">
105                                <div class="saddleback-dropdown form-selector">
106                                    <label for="meetingDayDropdown">
107                                        Meeting Day
108                                        <span class="required" v-if="!groupInfo.weeklyDayOfWeek">*</span>
109                                    </label>
110                                    <select id="meetingDayDropdown" :disabled="!canEdit">
111                                    </select>
112                                </div>
113                            </div>
114                        </div>
115
116                        <!-- Meeting Time -->
117                        <div class="meeting-time">
118                            <div class="form-input">
119                                <label for="meetingTimePrefix">
120                                    Meeting Time
121                                    <span class="required" v-if="!groupInfo.meetingTime">*</span>
122                                </label>
123                                <input type="text" id="meetingTimePrefix" class="saddleback-input" v-model="groupInfo.meetingTime" :disabled="!canEdit"/>
124                            </div>
125                            <div class="select-container">
126                                <div class="select-item">
127                                    <div class="saddleback-dropdown form-selector">
128                                        <select id="timeAmPmDropdown" v-model="groupInfo.meetingTimeAmPm" :disabled="!canEdit">
129                                            <option value="AM">AM</option>
130                                            <option value="PM">PM</option>
131                                        </select>
132                                    </div>
133                                </div>
134                            </div>
135                        </div>
136
137                        <!-- Time Zone -->
138                        <div class="select-container">
139                            <div class="select-item">
140                                <div class="saddleback-dropdown form-selector">
141                                    <label for="timeZoneDropdown">
142                                        Time Zone
143                                        <span class="required" v-if="!groupInfo.timeZone">*</span>
144                                    </label>
145                                    <select id="timeZoneDropdown" v-model="groupInfo.timeZone" :disabled="!canEdit">
146                                    </select>
147                                </div>
148                            </div>
149                        </div>
150
151                        <!-- Meeting Type -->
152                        <div class="meeting-type-container form-selector">
153                            <label>
154                                Meeting Type
155                                <span class="required" v-if="!groupInfo.meetingTypeId">*</span>
156                            </label>
157                            <div class="meeting-type radio-container">
158                                <!-- English -->
159                                <div v-if="!groupInfo.isFromSpanishStarterForm">
160                                    <div class="content-radio">
161                                        <input
162                                            type="radio"
163                                            id="inPerson"
164                                            name="meetingType"
165                                            :value="'49a09e61-11de-4238-be47-39f93e5e4b20'"
166                                            v-model="groupInfo.meetingTypeId"
167                                            :disabled="!canEdit"
168                                        />
169                                        <label for="inPerson">In-person</label>
170                                    </div>
171
172                                    <div class="content-radio">
173                                        <input
174                                            type="radio"
175                                            id="virtual"
176                                            name="meetingType"
177                                            :value="'0c8116cd-6941-4649-8400-24b37de8b5f0'"
178                                            v-model="groupInfo.meetingTypeId"
179                                            :disabled="!canEdit"
180                                        />
181                                        <label for="virtual">Virtual</label>
182                                    </div>
183                                </div>
184                                <!-- Español -->
185                                <div v-if="groupInfo.isFromSpanishStarterForm">
186                                    <div class="content-radio" v-for="option of this.spanishMeetingTypeOptions">
187                                        <input
188                                            type="radio"
189                                            :id="option.elementId"
190                                            name="meetingType"
191                                            :value="option.id"
192                                            v-model="groupInfo.meetingTypeId"
193                                            :disabled="!canEdit"
194                                        />
195                                        <label :for="option.elementId">{{option.text}}</label>
196                                    </div>
197                                </div>
198                            </div>
199                        </div>
200
201                        <!-- Address Section - Show only for In-person (meetingTypeId === 'In-Person') -->
202                        <div 
203                            class="address-flex"
204                            v-show="groupInfo.meetingTypeId == '49a09e61-11de-4238-be47-39f93e5e4b20' || groupInfo.meetingTypeId == spanishMeetingTypeOptions.find((o) => o.text === 'En persona')?.id">
205                            <div class="select-container">
206                                <div class="select-item">
207                                    <div class="saddleback-dropdown form-selector">
208                                        <label for="countryDropdown">Country</label>
209                                        <select id="countryDropdown" v-model="groupInfo.country" :disabled="!canEdit">
210                                        </select>
211                                    </div>
212                                </div>
213                            </div>
214                            <div class="form-input">
215                                <label for="streetAddressPrefix">
216                                    Street Address
217                                    <span class="required" v-if="!groupInfo.streetAddress">*</span>
218                                </label>
219                                <input type="text" id="streetAddressPrefix" class="saddleback-input" v-model="groupInfo.streetAddress" :disabled="!canEdit"/>
220                            </div>
221                            <div class="form-input">
222                                <label for="cityPrefix">
223                                    City
224                                    <span class="required" v-if="!groupInfo.city">*</span>
225                                </label>
226                                <input type="text" id="cityPrefix" class="saddleback-input" v-model="groupInfo.city" :disabled="!canEdit"/>
227                            </div>
228                            <div class="select-container">
229                                <div class="select-item">
230                                    <div class="saddleback-dropdown form-selector">
231                                        <label for="stateDropdown">{{regionLabel}}</label>
232                                        <select id="stateDropdown" v-model="groupInfo.region" :disabled="!canEdit">
233                                        </select>
234                                    </div>
235                                </div>
236                            </div>
237                            <div class="form-input">
238                                <label for="postalCodePrefix">
239                                    Postal Code
240                                    <span class="required" v-if="!groupInfo.postalCode">*</span>
241                                </label>
242                                <input type="text" id="postalCodePrefix" class="saddleback-input" v-model="groupInfo.postalCode" :disabled="!canEdit"/>
243                            </div>
244                        </div>
245
246                        <!-- Virtual Link Section - Show only for Virtual (meetingTypeId === '0c8116cd-6941-4649-8400-24b37de8b5f0') -->
247                        <div v-show="groupInfo.meetingTypeId === '0c8116cd-6941-4649-8400-24b37de8b5f0' || groupInfo.meetingTypeId === spanishMeetingTypeOptions.find((o) => o.text === 'Virtual')?.id">
248                            <div class="form-input">
249                                <label for="virtualLinkPrefix">
250                                    Meeting Link
251                                </label>
252                                <input type="url" id="virtualLinkPrefix" class="saddleback-input" v-model="groupInfo.meetingLink" :disabled="!canEdit" placeholder="e.g. Link to Meetup, Facebook event, Zoom, etc."/>
253                            </div>
254                        </div>
255                    </div>
256
257                    <div class="group-details-column">
258                        <!-- Group Type -->
259                        <div class="select-container">
260                            <div class="select-item">
261                                <!-- English -->
262                                <div class="saddleback-dropdown form-selector" v-if="!groupInfo.isFromSpanishStarterForm">
263                                    <label for="groupTypeDropdown">
264                                        Group Type
265                                        <span class="required" v-if="!groupInfo.smallGroupTypeId">*</span>
266                                    </label>
267                                    <select id="groupTypeDropdown" :disabled="!canEdit">
268                                    </select>
269                                </div>
270                                <!-- Español -->
271                                <div class="saddleback-dropdown form-selector" v-if="groupInfo.isFromSpanishStarterForm">
272                                    <label for="spanishGroupTypeDropdown">
273                                        Group Type
274                                        <span class="required" v-if="!groupInfo.smallGroupTypeId">*</span>
275                                    </label>
276                                    <select id="spanishGroupTypeDropdown" :disabled="!canEdit" style="display: none;">
277                                        <option v-for="option in spanishGroupTypeOptions" :id="option.id">{{option.text}}</option>
278                                    </select>
279                                </div>
280                            </div>
281                        </div>
282
283                        <!-- Group Capacity -->
284                        <div class="select-container">
285                            <div class="select-item">
286                                <div class="saddleback-dropdown form-selector">
287                                    <label for="groupCapacityDropdown">
288                                        Group Capacity (8-30)
289                                        <span class="required" v-if="!groupInfo.groupCapacity">*</span>
290                                    </label>
291                                    <select id="groupCapacityDropdown" :disabled="!canEdit">
292                                    </select>
293                                </div>
294                            </div>
295                        </div>
296
297                        <!--  Campus -->
298                        <div class="select-container">
299                            <div class="select-item">
300                                <div class="saddleback-dropdown form-selector">
301                                    <label for="associatedCampusDropdown">
302                                        Associated Campus
303                                        <span class="required" v-if="!groupInfo.campusId">*</span>
304                                    </label>
305                                    <select id="associatedCampusDropdown" v-model="groupInfo.campusId" :disabled="true">
306                                    </select>
307                                </div>
308                            </div>
309                        </div>
310                        
311                        <!-- Language -->
312                        <div class="select-container">
313                            <div class="select-item">
314                                <div class="saddleback-dropdown form-selector">
315                                    <label for="languageDropdown">
316                                        Language
317                                        <span class="required" v-if="!groupInfo.languageValue">*</span>
318                                    </label>
319                                    <select id="languageDropdown" v-model="groupInfo.languageValue" :disabled="!canEdit">
320                                    </select>
321                                </div>
322                            </div>
323                        </div>
324
325                        <!-- Age Groups -->
326                        <div class="checkbox-container form-selector">
327                            <label class="checkbox-container-label">
328                                Age Groups
329                                <span class="required" v-if="!groupInfo.ageGroupIds || groupInfo.ageGroupIds.length === 0">*</span>
330                            </label>
331                            <div class="checkbox-inner">
332                                <div class="checkbox-group" v-for="ageOption in ageOptions" :key="ageOption.id">
333                                    <input 
334                                        type="checkbox" 
335                                        :id="ageOption.id" 
336                                        name="ageGroups" 
337                                        :value="ageOption.id" 
338                                        v-model="groupInfo.ageGroupIds"
339                                        :disabled="!canEdit"
340                                    >
341                                    <label :for="ageOption.id">{{ageOption.text}}</label>
342                                </div>
343                            </div>   
344                        </div>
345
346                        <!-- Life Stages -->
347                        <div class="checkbox-container form-selector">
348                            <label class="checkbox-container-label">
349                                Life Stages
350                                <span class="required" v-if="!groupInfo.lifeStageIds || groupInfo.lifeStageIds.length === 0">*</span>
351                            </label>
352                            <!-- English -->
353                            <div class="checkbox-inner" v-if="!groupInfo.isFromSpanishStarterForm">
354                                <div class="checkbox-group" v-for="lifeStageOption in lifeStageOptions" :key="lifeStageOption.id">
355                                    <input 
356                                        type="checkbox" 
357                                        :id="lifeStageOption.id" 
358                                        name="lifeStages" 
359                                        :value="lifeStageOption.id" 
360                                        v-model="groupInfo.lifeStageIds"
361                                        :disabled="!canEdit"
362                                    >
363                                    <label :for="lifeStageOption.id">{{lifeStageOption.text}}</label>
364                                </div>
365                            </div>
366                            <!-- Español -->
367                            <div class="checkbox-inner" v-if="groupInfo.isFromSpanishStarterForm">
368                                <div class="checkbox-group" v-for="lifeStageOption in spanishLifeStageOptions" :key="lifeStageOption.id">
369                                    <input
370                                        type="checkbox"
371                                        :id="lifeStageOption.id"
372                                        name="lifeStages"
373                                        :value="lifeStageOption.id"
374                                        v-model="groupInfo.lifeStageIds"
375                                        :disabled="!canEdit"
376                                    >
377                                    <label :for="lifeStageOption.id">{{lifeStageOption.text}}</label>
378                                </div>
379                            </div>
380                        </div>
381
382                        <!-- Gender -->
383                        <div class="select-container">
384                            <div class="select-item">
385                                <div class="saddleback-dropdown form-selector">
386                                    <label for="genderDropdown">
387                                        Gender
388                                        <span class="required" v-if="!groupInfo.genderValue">*</span>
389                                    </label>
390                                    <select id="genderDropdown" v-model="groupInfo.genderValue" :disabled="!canEdit">
391                                    </select>
392                                </div>
393                            </div>
394                        </div>
395
396                        <!-- Kids Welcome -->
397                        <div class="kids-welcome-container form-selector">
398                            <label>
399                                Kids Welcome
400                                <span class="required" v-if="groupInfo.kidsWelcome === null || groupInfo.kidsWelcome === undefined">*</span>
401                            </label>
402                            <div class="kids-welcome radio-container">
403                                <div class="content-radio">
404                                    <input
405                                        type="radio"
406                                        id="kidsWelcomeYes"
407                                        name="kidsWelcome"
408                                        value="True"
409                                        v-model="kidsWelcomeForDisplay"
410                                        :disabled="!canEdit"
411                                    />
412                                    <label for="kidsWelcomeYes">Yes</label>
413                                </div>
414
415
416                                <div class="content-radio">
417                                    <input
418                                        type="radio"
419                                        id="kidsWelcomeNo"
420                                        name="kidsWelcome"
421                                        value="False"
422                                        v-model="kidsWelcomeForDisplay"
423                                    />
424                                    <label for="kidsWelcomeNo">No</label>
425                                </div>
426                            </div>
427                        </div>
428                    </div>
429                </div>   
430                
431                <!-- Save Button --> 
432                <div class="info-tab-btn-container">
433                    <button class="btn invert" :disabled="!isModified || !canEdit" v-on:click="handleCancel">Cancel</button>
434                    <button class="btn inactive submit" :disabled="!isValid || !isModified || !canEdit" v-on:click="handleSave">Save</button>
435                </div>             
436            </form>
437        </div>
438
439        <div class="my-groups-container" v-show="visible && isParticipant">
440            <div class="group-photo-container">
441                <div class="group-photo">
442                    <div class="photo-image" v-if="groupInfo.profilePictureUrl">
443                        <img
444                            class="photo"
445                            :src="rockDomain + groupInfo.profilePictureUrl"
446                            alt=""
447                        />
448                    </div>
449                    <div class="photo-placeholder" v-if="!groupInfo.profilePictureUrl">
450                        <img
451                            class="svg-overlay"
452                            :src="groupInfo.smallGroupTypeId ? '/small-group-types-thumbnails/thumbnail-' + this.mapThumbnailIndex(groupInfo.smallGroupTypeId) + '.png' :
453                            '/small-group-types-thumbnails/thumbnail-default.png'"
454                            alt=""
455                        />
456                    </div>
457                </div>
458                <div class="group-photo-spacer"></div>
459            </div>
460            <div class="group-participant-details-container">
461                <div class="group-participant-details-column">
462                    <div class="group-block">
463                        <div class="block-name">Group Name</div>
464                        <div class="block-value">{{groupInfo.name}}</div>
465                    </div>
466                    <div class="group-block">
467                        <div class="block-name">Group Description</div>
468                        <div class="block-value">{{groupInfo.description}}</div>
469                    </div>
470                    <div class="group-block">
471                        <div class="block-name">Meeting Day/Time</div>
472                        <div class="block-value">{{meetingTimeForDisplay}}</div>
473                    </div>
474                    <div class="group-block">
475                        <div class="block-name">Meeting Type</div>
476                        <div class="block-value">{{groupInfo.meetingTypeId === '49a09e61-11de-4238-be47-39f93e5e4b20' ? 'In-person' : 'Virtual'}}</div>
477                    </div>
478                    <div class="group-block" v-if="groupInfo.meetingTypeId === '49a09e61-11de-4238-be47-39f93e5e4b20'">
479                        <div class="block-name">Location</div>
480                        <div class="block-value">{{locationFormatted}}</div>
481                    </div>
482                    <div class="group-block" v-else="groupInfo.meetingTypeId === '0c8116cd-6941-4649-8400-24b37de8b5f0'">
483                        <div class="block-name">Meeting Link</div>
484                        <div class="block-value">
485                            {{groupInfo.meetingLink == null ? 'Unknown' : groupInfo.meetingLink}}
486                        </div>
487                    </div>
488                    <div class="group-block">
489                        <div class="block-name">Group Type</div>
490                        <div class="block-value">{{groupType}}</div>
491                    </div>
492                    <div class="group-block">
493                        <div class="block-name">Group Capacity</div>
494                        <div class="block-value">{{groupInfo.groupCapacity}}</div>
495                    </div>
496                </div>
497
498                <div class="group-participant-details-column">
499                    <div class="group-block">
500                        <div class="block-name">Associated Campus</div>
501                        <div class="block-value">{{campusName}}</div>
502                    </div>
503                    <div class="group-block">
504                        <div class="block-name">Language</div>
505                        <div class="block-value">{{language}}</div>
506                    </div>
507                    <div class="group-block">
508                        <div class="block-name">Age Groups</div>
509                        <div class="block-value">{{ageGroup}}</div>
510                    </div>
511                    <div class="group-block">
512                        <div class="block-name">Life Stage</div>
513                        <div class="block-value">{{lifeStage}}</div>
514                    </div>
515                    <div class="group-block">
516                        <div class="block-name">Gender</div>
517                        <div class="block-value">{{gender}}</div>
518                    </div>
519                    <div class="group-block">
520                        <div class="block-name">Kids Welcome</div>
521                        <div class="block-value">
522                            {{groupInfo.kidsWelcome == null ? 'Unknown' : 
523                                groupInfo.kidsWelcome ? 'Yes' : 'No'}}
524                        </div>
525                    </div>                    
526                </div>
527            </div>
528        </div>
529
530        <GroupImageModal
531          :visible="isGroupImageModalVisible"
532          :groupId="groupId"
533          :healthyChurchApi="healthyChurchApi"
534          :healthyChurchSmallGroupsApi="healthyChurchSmallGroupsApi"
535          :rockDomain="rockDomain"
536          :authHeaders="authHeaders"
537          :currentImageUrl="groupInfo.profilePictureUrl ? (rockDomain + groupInfo.profilePictureUrl) : null"
538          v-on:close="showGroupImageModal(false)"
539          v-on:uploaded="handleImageUpload"
540        />
541    `,
542
543  data() {
544    return {
545      debug: new URLSearchParams(window.location.search).has('debug'),
546      isGroupImageModalVisible: false,
547      groupInfo: {},
548      isSaving: false,
549      isSpanishGroup: false,
550      hasInitializedSpanishGroupTypeDropDown: false,
551
552    };
553  },
554
555  computed: {
556    ageGroup() {
557      return this.stringJoin(this.groupInfo.ageGroup);
558    },
559
560    lifeStage() {
561      if (this.groupInfo.lifeStageIds && this.groupInfo.lifeStageIds.length > 0) {
562        return this.groupInfo.lifeStageIds
563          .map((id) => ROCK_LIFE_STAGE_OPTIONS.find((option) => option.id === id)?.text)
564          .filter(Boolean)
565          .join(', ');
566      }
567      return '';
568    },
569
570    regionLabel() {
571      return this.regionLabels[this.groupInfo.address?.country] || 'State/Province';
572    },
573
574    isValid() {
575      const baseRequirements = [
576        this.groupInfo.name,
577        this.groupInfo.description,
578        this.groupInfo.meetingTime,
579        this.groupInfo.meetingTimeAmPm,
580        this.groupInfo.smallGroupTypeId,
581        this.groupInfo.campusId, // Associated Campus
582        this.groupInfo.languageValue,
583        this.groupInfo.ageGroupIds && this.groupInfo.ageGroupIds.length > 0,
584        this.groupInfo.lifeStageIds && this.groupInfo.lifeStageIds.length > 0,
585        this.groupInfo.genderValue,
586        this.groupInfo.kidsWelcome !== null && this.groupInfo.kidsWelcome !== undefined,
587        this.groupInfo.meetingTypeId,
588        this.groupInfo.weeklyDayOfWeek,
589        this.groupInfo.weeklyTimeOfDay,
590        this.groupInfo.timeZone,
591      ];
592
593      if (this.groupInfo.meetingTypeId === ROCK_MEETING_TYPE.IN_PERSON) {
594        // In-person: require address fields
595        const addressRequirements = [
596          this.groupInfo.streetAddress,
597          this.groupInfo.city,
598          this.groupInfo.region,
599          this.groupInfo.postalCode,
600          this.groupInfo.country,
601        ];
602        return baseRequirements.every(Boolean) && addressRequirements.every(Boolean);
603      } else if (this.groupInfo.meetingTypeId === ROCK_MEETING_TYPE.VIRTUAL) {
604        // Virtual: meeting link is optional, so no additional requirements
605        return baseRequirements.every(Boolean);
606      }
607
608      return baseRequirements.every(Boolean);
609    },
610
611    isModified() {
612      const isEqual = JSON.stringify(this.groupInfo) === JSON.stringify(this.originalGroupInfo);
613      return !isEqual;
614    },
615
616    canEdit() {
617      return !this.isSaving && !this.isFetching;
618    },
619
620    groupId() {
621      return this.groupInfo.id;
622    },
623
624    streetAddress: {
625      get() {
626        return this.groupInfo.address?.addressLine1 || '';
627      },
628      set(value) {
629        if (!this.groupInfo.address) {
630          this.groupInfo.address = {};
631        }
632        this.groupInfo.address.addressLine1 = value;
633      },
634    },
635
636    campusName() {
637      return this.groupInfo.campusName;
638    },
639
640    city: {
641      get() {
642        return this.groupInfo.address?.city || '';
643      },
644      set(value) {
645        if (!this.groupInfo.address) {
646          this.groupInfo.address = {};
647        }
648        this.groupInfo.address.city = value;
649      },
650    },
651
652    country: {
653      get() {
654        return this.groupInfo.address?.country || '';
655      },
656      set(value) {
657        if (!this.groupInfo.address) {
658          this.groupInfo.address = {};
659        }
660        this.groupInfo.address.country = value;
661      },
662    },
663
664    gender() {
665      return this.stringJoin(this.groupInfo.gender);
666    },
667
668    groupType() {
669      const groupTypeOption =
670        ROCK_GROUP_TYPE_OPTIONS.find((option) => option.id === this.groupInfo.smallGroupTypeId) ??
671        this.spanishGroupTypeOptions.find((option) => option.id == this.groupInfo.smallGroupTypeId);
672
673      return groupTypeOption?.text ?? 'Unknown Group Type';
674    },
675
676    language() {
677      return this.stringJoin(this.groupInfo.language);
678    },
679
680    lifeStage() {
681      return this.stringJoin(this.groupInfo.lifeStage);
682    },
683
684    locationFormatted() {
685      if (this.groupInfo.locations && this.groupInfo.locations[0]) {
686        const { address1, address2, city, region, postalCode, country } = this.groupInfo.locations[0];
687
688        let result = `${address1} ${address2 ?? ''}`.trim();
689        result += `, ${city}, ${region} ${postalCode}, ${country}`;
690        return result;
691      }
692
693      return '';
694    },
695
696    postalCode: {
697      get() {
698        return this.groupInfo.address?.postalCode || '';
699      },
700      set(value) {
701        if (!this.groupInfo.address) {
702          this.groupInfo.address = {};
703        }
704        this.groupInfo.address.postalCode = value;
705      },
706    },
707
708    region: {
709      get() {
710        return this.groupInfo.address?.region || '';
711      },
712      set(value) {
713        if (!this.groupInfo.address) {
714          this.groupInfo.address = {};
715        }
716        this.groupInfo.address.region = value;
717      },
718    },
719
720    meetingTime: {
721      get() {
722        return this.groupInfo.weeklyTimeOfDay;
723      },
724      set(value) {
725        this.groupInfo.schedule.sequences[0].startTime = value;
726      },
727    },
728
729    meetingTimeForDisplay() {
730      const { weeklyDayOfWeek, weeklyTimeOfDay } = this.groupInfo;
731
732      let formattedTimeOfDay = '';
733
734      if (weeklyTimeOfDay) {
735        var parsedTime = dayjs(weeklyTimeOfDay, 'HH:mm:ss').toDate().getHours();
736        formattedTimeOfDay = dayjs().startOf('day').add(dayjs.duration({ hours: parsedTime })).format('h:mm A'); // TODO: Do we care about localization?
737      }
738
739      return `${weeklyDayOfWeek ?? ''} ${formattedTimeOfDay}`.trim();
740    },
741
742    kidsWelcomeForDisplay: {
743      get() {
744        return this.groupInfo.kidsWelcome === true ? 'True' : 'False';
745      },
746      set(value) {
747        this.groupInfo.kidsWelcome = value === 'True';
748      },
749    },
750
751    lifeStageOptions() {
752      return ROCK_LIFE_STAGE_OPTIONS;
753    },
754
755    ageOptions() {
756      return ROCK_AGES_OPTIONS;
757    },
758  },
759
760  mounted() {
761    const campuses = this.rockCampuses.map((v) => ({
762      id: v.id,
763      text: v.name,
764    }));
765
766    console.log('[MyGroupsApp][Info Tab] Campuses:', campuses);
767
768    $('#groupsDropdown').select2({
769      minimumResultsForSearch: Infinity,
770      mobile: true,
771      width: '100%',
772    });
773
774    $('#timeAmPmDropdown')
775      .select2({
776        minimumResultsForSearch: Infinity,
777        mobile: true,
778        width: '100%',
779      })
780      .on('change', (e) => {
781        this.groupInfo.meetingTimeAmPm = e.target.value;
782      });
783
784    $('#stateDropdown')
785      .select2({
786        minimumResultsForSearch: 0,
787        mobile: true,
788        width: '100%',
789      })
790      .on('change', (e) => {
791        this.groupInfo.region = e.target.value;
792      });
793
794    $('#countryDropdown')
795      .select2({
796        minimumResultsForSearch: 0,
797        mobile: true,
798        width: '100%',
799        data: this.countryList,
800      })
801      .on('change', (e) => {
802        // Load regions when country changes
803        this.getRegionList(e.target.value);
804        this.groupInfo.country = e.target.value;
805      });
806
807    $('#associatedCampusDropdown')
808      .select2({
809        minimumResultsForSearch: Infinity,
810        mobile: true,
811        width: '100%',
812        data: campuses,
813      })
814      .on('change', (e) => {
815        this.groupInfo.campusId = e.target.value;
816      });
817
818    $('#groupCapacityDropdown')
819      .select2({
820        minimumResultsForSearch: Infinity,
821        mobile: true,
822        width: '100%',
823        data: GROUP_CAPACITY_OPTIONS,
824      })
825      .on('change', (e) => {
826        this.groupInfo.groupCapacity = parseInt(e.target.value, 10);
827      });
828
829    $('#timeZoneDropdown')
830      .select2({
831        minimumResultsForSearch: 0,
832        mobile: true,
833        width: '100%',
834        data: ROCK_TIMEZONE_OPTIONS.map((v) => ({
835          id: v.id,
836          text: v.text,
837        })),
838      })
839      .on('change', (e) => {
840        this.groupInfo.timeZone = e.target.value;
841      });
842
843    $('#groupTypeDropdown')
844      .select2({
845        data: ROCK_GROUP_TYPE_OPTIONS,
846        minimumResultsForSearch: Infinity,
847        mobile: true,
848        width: '100%',
849      })
850      .on('change', (e) => {
851        this.groupInfo.smallGroupTypeId = e.target.value;
852      });
853
854    // Spanish group Type Dropdown
855    // Bind Change handler
856    // But we cannot setup "select2" because we don't have the options data from the API yet
857    // We have a watcher to help with that
858    $('#spanishGroupTypeDropdown').on('change', (e) => {
859      console.log('Spanish group Type changed!');
860      this.groupInfo.smallGroupTypeId = e.target.value;
861    });
862
863    $('#meetingDayDropdown')
864      .select2({
865        data: DAYS_OPTIONS,
866        minimumResultsForSearch: Infinity,
867        mobile: true,
868        width: '100%',
869      })
870      .on('change', (e) => {
871        this.groupInfo.weeklyDayOfWeek = e.target.value;
872      });
873
874    $('#languageDropdown')
875      .select2({
876        data: ROCK_LANGUAGE_OPTIONS,
877        minimumResultsForSearch: 0,
878        mobile: true,
879        width: '100%',
880      })
881      .on('change', (e) => {
882        this.groupInfo.languageValue = e.target.value;
883      });
884
885    $('#genderDropdown')
886      .select2({
887        data: ROCK_GENDER_OPTIONS,
888        minimumResultsForSearch: Infinity,
889        mobile: true,
890        width: '100%',
891      })
892      .on('change', (e) => {
893        this.groupInfo.genderValue = e.target.value;
894      });
895  },
896
897  watch: {
898      originalGroupInfo(newVal, oldVal) {
899        console.log('[MyGroupsApp][Info Tab] Watcher for Original Group Info is firing...');
900
901        if (newVal) {
902          // Create a deep copy and normalize GUID casing
903          this.groupInfo = JSON.parse(JSON.stringify(newVal));
904
905          console.log('[MyGroupsApp][Info Tab] Watcher for Original Group Info - New group:', this.groupInfo);
906          console.log('[MyGroupsApp][Info Tab] Watcher for Original Group Info - New group is Spanish:', this.groupInfo.isFromSpanishStarterForm);
907
908          if (this.groupInfo.isFromSpanishStarterForm && !this.hasInitializedSpanishGroupTypeDropDown) {
909              console.log('[MyGroupsApp][Info Tab] Watcher for Original Group Info - We have not initialized Spanish Group Types Dropdown Yet');
910              console.log('[MyGroupsApp][Info Tab] Watcher for Original Group Info - Options:', this.spanishGroupTypeOptions);
911
912              // TODO/FIXME: Not working :-(
913              setTimeout(() => {
914                $('#spanishGroupTypeDropdown')
915                  .select2({
916                      minimumResultsForSearch: 0,
917                      mobile: true,
918                      width: '100%',
919                      // data: JSON.parse(JSON.stringify(this.spanishGroupTypeOptions)),
920                  });
921              }, 100);
922
923              this.hasInitializedSpanishGroupTypeDropDown = true;
924          }
925
926          // Helper function to find matching option with case-insensitive comparison
927          const findMatchingOption = (apiId, optionsArray) => {
928            if (!apiId) return null;
929            return optionsArray.find(option => 
930              option.id.toLowerCase() === apiId.toLowerCase()
931            );
932          };
933
934          // Extract and match language ID
935          let apiLanguageId = newVal.languageIds && newVal.languageIds.length > 0 
936            ? newVal.languageIds[0] 
937            : null;
938          
939          const languageMatch = findMatchingOption(apiLanguageId, ROCK_LANGUAGE_OPTIONS);
940          this.groupInfo.languageValue = languageMatch ? languageMatch.id : apiLanguageId;
941
942          // Handle Life Stage (checkboxes) - case-insensitive matching for array
943          if (newVal.lifeStageIds && Array.isArray(newVal.lifeStageIds)) {
944            this.groupInfo.lifeStageIds = newVal.lifeStageIds.map(apiLifeStageId => {
945              const lifeStageMatch = findMatchingOption(apiLifeStageId, ROCK_LIFE_STAGE_OPTIONS);
946              return lifeStageMatch ? lifeStageMatch.id : apiLifeStageId;
947            });
948          } else {
949            this.groupInfo.lifeStageIds = [];
950          }
951
952          // Extract and match gender ID
953          let apiGenderId = newVal.genderIds && newVal.genderIds.length > 0 
954            ? newVal.genderIds[0] 
955            : null;
956          
957          const genderMatch = findMatchingOption(apiGenderId, ROCK_GENDER_OPTIONS);
958          this.groupInfo.genderValue = genderMatch ? genderMatch.id : apiGenderId;
959
960          // Handle Meeting Type (radio buttons) - case-insensitive matching
961          if (newVal.meetingTypeId) {
962            const meetingTypeMatch = findMatchingOption(newVal.meetingTypeId, ROCK_MEETING_TYPE_OPTIONS);
963            this.groupInfo.meetingTypeId = meetingTypeMatch ? meetingTypeMatch.id : newVal.meetingTypeId;
964          }
965
966          // Handle Age Groups (checkboxes) - case-insensitive matching for array
967          if (newVal.ageGroupIds && Array.isArray(newVal.ageGroupIds)) {
968            this.groupInfo.ageGroupIds = newVal.ageGroupIds.map(apiAgeId => {
969              const ageMatch = findMatchingOption(apiAgeId, ROCK_AGES_OPTIONS);
970              return ageMatch ? ageMatch.id : apiAgeId;
971            });
972          } else {
973            this.groupInfo.ageGroupIds = [];
974          }
975
976          // Handle address/location data extraction
977          if (newVal.locations && newVal.locations.length > 0) {
978            const location = newVal.locations[0];
979            this.groupInfo.country = location.country;
980            this.groupInfo.region = location.region;
981            this.groupInfo.streetAddress = location.address1;
982            this.groupInfo.city = location.city;
983            this.groupInfo.postalCode = location.postalCode;
984          } else if (newVal.location) {
985            this.groupInfo.country = newVal.location.country;
986            this.groupInfo.region = newVal.location.region;
987            this.groupInfo.streetAddress = newVal.location.address1;
988            this.groupInfo.city = newVal.location.city;
989            this.groupInfo.postalCode = newVal.location.postalCode;
990          } else if (newVal.address) {
991            this.groupInfo.country = newVal.address.country;
992            this.groupInfo.region = newVal.address.region;
993            this.groupInfo.streetAddress = newVal.address.addressLine1;
994            this.groupInfo.city = newVal.address.city;
995            this.groupInfo.postalCode = newVal.address.postalCode;
996          }
997
998          // IMPORTANT: Also normalize the original data to prevent false "modified" detection
999          // Apply the same transformations to the source arrays so comparison works correctly
1000          if (this.groupInfo.languageValue && newVal.languageIds) {
1001            newVal.languageIds[0] = this.groupInfo.languageValue;
1002          }
1003          if (this.groupInfo.lifeStages && newVal.lifeStageIds) {
1004            newVal.lifeStageIds = [...this.groupInfo.lifeStageIds];
1005          }
1006          if (this.groupInfo.genderValue && newVal.genderIds) {
1007            newVal.genderIds[0] = this.groupInfo.genderValue;
1008          }
1009          if (this.groupInfo.meetingTypeId && newVal.meetingTypeId) {
1010            newVal.meetingTypeId = this.groupInfo.meetingTypeId;
1011          }
1012          if (this.groupInfo.ageGroupIds && newVal.ageGroupIds) {
1013            newVal.ageGroupIds = [...this.groupInfo.ageGroupIds];
1014          }
1015
1016          // Set up dropdowns in the correct order - FIX THE TIMING ISSUE
1017          this.$nextTick(() => {
1018            // Set country first, but DON'T try to set region immediately
1019            if (this.groupInfo.country) {
1020              $('#countryDropdown').val(this.groupInfo.country).trigger('change');
1021              
1022              // Wait a bit longer before setting up regions to avoid race conditions
1023              setTimeout(() => {
1024                if (this.groupInfo.region && this.groupInfo.country) {
1025                  // Double-check the region dropdown is ready and set the value
1026                  const stateDropdown = $('#stateDropdown');
1027                  if (stateDropdown.length && stateDropdown.find('option').length > 1) {
1028                    stateDropdown.val(this.groupInfo.region).trigger('change.select2');
1029                  }
1030                }
1031              }, 500);
1032            }
1033
1034            // Set other dropdowns (these don't depend on async calls)
1035            $('#languageDropdown').val(this.groupInfo.languageValue).trigger('change.select2');
1036            $('#associatedCampusDropdown').val(this.groupInfo.campusId).trigger('change.select2');
1037            $('#groupCapacityDropdown').val(`${this.groupInfo.groupCapacity}_groupCap`).trigger('change.select2');
1038            $('#groupTypeDropdown').val(this.groupInfo.smallGroupTypeId).trigger('change.select2');
1039            $('#genderDropdown').val(this.groupInfo.genderValue).trigger('change.select2');
1040            $('#timeZoneDropdown').val(this.groupInfo.timeZone).trigger('change.select2');
1041            $('#meetingDayDropdown').val(this.groupInfo.weeklyDayOfWeek).trigger('change.select2');
1042            $('#timeAmPmDropdown').val(this.groupInfo.meetingTimeAmPm).trigger('change.select2');
1043          });
1044        }
1045      },
1046      spanishGroupTypeOptions: {
1047          handler(newVal, oldVal) {
1048              if (!oldVal && newVal && newVal.length) {
1049                  console.log('[MyGroupsApp][Info Tab] Watcher on Spanish Group Options -- Data is Available');
1050              }
1051          },
1052          immediate: true,
1053          deep: true,
1054      },
1055  },
1056
1057  methods: {
1058    setupTabNavigation() {
1059      // Set up keyboard navigation for all existing Select2 dropdowns
1060      const self = this;
1061      $('select').each(function () {
1062        const $select = $(this);
1063        if ($select.hasClass('select2-hidden-accessible')) {
1064          self.setupSingleDropdownNavigation($select);
1065        }
1066      });
1067    },
1068
1069    setupSingleDropdownNavigation($select) {
1070      const $container = $select.next('.select2-container');
1071
1072      // Remove tabindex from the original select element
1073      $select.attr('tabindex', '-1');
1074
1075      // Make the container focusable
1076      $container.attr('tabindex', '0');
1077
1078      // Remove any existing event handlers to avoid duplicates
1079      $container.off('keydown.tabNavigation focus.tabNavigation blur.tabNavigation keypress.tabNavigation');
1080
1081      // Variables for type-to-search functionality
1082      let searchString = '';
1083      let searchTimeout;
1084
1085      $container.on('keydown.tabNavigation', function (e) {
1086        const $this = $(this);
1087
1088        // Space or Enter to open dropdown
1089        if (e.which === 32 || e.which === 13) {
1090          // Space or Enter
1091          e.preventDefault();
1092          $this.prev('select').select2('open');
1093        }
1094
1095        // Arrow keys to navigate options when closed
1096        if (e.which === 38 || e.which === 40) {
1097          // Up or Down arrow
1098          e.preventDefault();
1099          const $selectEl = $this.prev('select');
1100          const currentVal = $selectEl.val();
1101          const options = $selectEl.find('option').not('[value=""]'); // Exclude empty options
1102          let currentIndex = -1;
1103
1104          // Find current index
1105          for (let i = 0; i < options.length; i++) {
1106            if ($(options[i]).val() === currentVal) {
1107              currentIndex = i;
1108              break;
1109            }
1110          }
1111
1112          // Calculate new index
1113          if (e.which === 38 && currentIndex > 0) {
1114            // Up arrow
1115            currentIndex--;
1116          } else if (e.which === 40 && currentIndex < options.length - 1) {
1117            // Down arrow
1118            currentIndex++;
1119          } else if (e.which === 40 && currentIndex === -1) {
1120            // Down arrow from no selection
1121            currentIndex = 0;
1122          }
1123
1124          if (currentIndex >= 0 && currentIndex < options.length) {
1125            const newValue = $(options[currentIndex]).val();
1126            if (newValue && newValue !== currentVal) {
1127              // Close dropdown if open to prevent visual conflicts
1128              if ($selectEl.select2('isOpen')) {
1129                $selectEl.select2('close');
1130              }
1131
1132              $selectEl.val(newValue).trigger('change');
1133            }
1134          }
1135        }
1136      });
1137
1138      // Handle typing to search
1139      $container.on('keypress.tabNavigation', function (e) {
1140        // Only handle letter/number keys
1141        if (e.which >= 32 && e.which <= 126) {
1142          const char = String.fromCharCode(e.which).toLowerCase();
1143          const $selectEl = $container.prev('select');
1144
1145          // Clear previous timeout
1146          if (searchTimeout) {
1147            clearTimeout(searchTimeout);
1148          }
1149
1150          // Add character to search string
1151          searchString += char;
1152
1153          // Find matching option
1154          const options = $selectEl.find('option').not('[value=""]');
1155          let matchFound = false;
1156
1157          for (let i = 0; i < options.length; i++) {
1158            const optionText = $(options[i]).text().toLowerCase();
1159            if (optionText.startsWith(searchString)) {
1160              const newValue = $(options[i]).val();
1161              if (newValue) {
1162                // Close dropdown if open
1163                if ($selectEl.select2('isOpen')) {
1164                  $selectEl.select2('close');
1165                }
1166
1167                $selectEl.val(newValue).trigger('change');
1168                matchFound = true;
1169                break;
1170              }
1171            }
1172          }
1173
1174          // If no match found with full string, try just the new character
1175          if (!matchFound) {
1176            searchString = char;
1177            for (let i = 0; i < options.length; i++) {
1178              const optionText = $(options[i]).text().toLowerCase();
1179              if (optionText.startsWith(searchString)) {
1180                const newValue = $(options[i]).val();
1181                if (newValue) {
1182                  if ($selectEl.select2('isOpen')) {
1183                    $selectEl.select2('close');
1184                  }
1185
1186                  $selectEl.val(newValue).trigger('change');
1187                  break;
1188                }
1189              }
1190            }
1191          }
1192
1193          // Clear search string after 1 second of no typing
1194          searchTimeout = setTimeout(() => {
1195            searchString = '';
1196          }, 1000);
1197        }
1198      });
1199
1200      // Visual focus indicator
1201      $container
1202        .on('focus.tabNavigation', function () {
1203          $(this).addClass('select2-container--focus');
1204        })
1205        .on('blur.tabNavigation', function () {
1206          $(this).removeClass('select2-container--focus');
1207          // Clear search string on blur
1208          searchString = '';
1209          if (searchTimeout) {
1210            clearTimeout(searchTimeout);
1211          }
1212        });
1213    },
1214
1215    handleCancel() {
1216      this.groupInfo = JSON.parse(JSON.stringify(this.originalGroupInfo));
1217
1218      // Update all Select2 dropdowns to reflect the reverted data
1219      this.$nextTick(() => {
1220        $('#languageDropdown').val(this.groupInfo.languageValue).trigger('change.select2');
1221        $('#genderDropdown').val(this.groupInfo.genderValue).trigger('change.select2');
1222        $('#associatedCampusDropdown').val(this.groupInfo.campusId).trigger('change.select2');
1223        $('#groupCapacityDropdown').val(`${this.groupInfo.groupCapacity}_groupCap`).trigger('change.select2');
1224        $('#groupTypeDropdown').val(this.groupInfo.smallGroupTypeId).trigger('change.select2');
1225        $('#timeZoneDropdown').val(this.groupInfo.timeZone).trigger('change.select2');
1226        $('#meetingDayDropdown').val(this.groupInfo.weeklyDayOfWeek).trigger('change.select2');
1227        $('#timeAmPmDropdown').val(this.groupInfo.meetingTimeAmPm).trigger('change.select2');
1228
1229        // Handle country/region dropdowns (country first, then region)
1230        if (this.groupInfo.country) {
1231          $('#countryDropdown').val(this.groupInfo.country).trigger('change');
1232          // Region will be set automatically when country change triggers getRegionList
1233        }
1234      });
1235    },
1236
1237    async handleSave() {
1238      this.isSaving = true;
1239
1240      try {
1241        let locationData = null;
1242
1243        // Handle geocoding for in-person meetings (only international, not US)
1244        if (this.groupInfo.meetingTypeId === ROCK_MEETING_TYPE.IN_PERSON) {
1245          // Build address inline
1246          const fullAddress = [
1247            this.groupInfo.streetAddress,
1248            this.groupInfo.city,
1249            this.groupInfo.region,
1250            this.groupInfo.postalCode,
1251            this.groupInfo.country,
1252          ].filter(Boolean).join(', ');
1253
1254          // Start with basic location data
1255          locationData = {
1256            address1: this.groupInfo.streetAddress,
1257            city: this.groupInfo.city,
1258            region: this.groupInfo.region,
1259            postalCode: this.groupInfo.postalCode,
1260            country: this.groupInfo.country,
1261          };
1262
1263          // Only geocode for international groups (non-US)
1264          if (fullAddress && this.groupInfo.country !== 'US') {
1265            try {
1266              const coordinates = await this.geocodeAddress(fullAddress);
1267              locationData.latitude = coordinates.latitude;
1268              locationData.longitude = coordinates.longitude;
1269            } catch (geocodeError) {
1270              console.error('Geocoding failed for international address:', geocodeError);
1271              // locationData already has address info, just missing coordinates
1272            }
1273          } 
1274        }
1275
1276        // Create clean payload
1277        const payload = {
1278          ...this.groupInfo,
1279
1280          // 1. Set type based on meetingTypeId
1281          type: this.groupInfo.meetingTypeId === ROCK_MEETING_TYPE.IN_PERSON ? 0 : 2, // 0=HomeGroup, 2=OnlineGroup
1282
1283          // 2. Handle meetingLink (only for virtual meetings)
1284          meetingLink: this.groupInfo.meetingTypeId === ROCK_MEETING_TYPE.VIRTUAL ? this.groupInfo.meetingLink : null,
1285
1286          // 3. Fix gender mapping
1287          genderIds: this.groupInfo.genderValue ? [this.groupInfo.genderValue] : null,
1288
1289          // 5. Fix language mapping
1290          languageIds: this.groupInfo.languageValue ? [this.groupInfo.languageValue] : null,
1291
1292          // 6. Combine time fields into a single time string
1293          weeklyTimeOfDay: this.combineTimeFields(this.groupInfo.meetingTime, this.groupInfo.meetingTimeAmPm),
1294
1295          // 7. Build out the location object
1296          location: locationData,
1297
1298          // 8. Handle profile picture Id
1299          profilePictureId: this.groupInfo.profilePictureId || null,
1300
1301          // 6. Remove form-only fields that shouldn't be sent to API
1302          meetingTime: undefined,
1303          meetingTimeAmPm: undefined,
1304          virtualLink: undefined,
1305          languageText: undefined,
1306          genderText: undefined,
1307          typeText: undefined,
1308          ageGroupsText: undefined,
1309          ageGroup: undefined,
1310          lifeStagesText: undefined,
1311          campusName: undefined,
1312          address: undefined,
1313        };
1314
1315        // Remove undefined fields
1316        Object.keys(payload).forEach((key) => payload[key] === undefined && delete payload[key]);
1317
1318        const apiUrl = `${this.healthyChurchApi}/api/v4/smallgroups`;
1319
1320        const response = await axios.put(apiUrl, payload, this.authHeaders);
1321
1322        // SUCCESS: Only emit on successful save
1323        this.$emit('refetch-group', { 
1324          title: 'Success!',
1325          message: 'Group Info has been updated' 
1326        });
1327      } catch (error) {
1328        console.error('Error in handleSave:', error);
1329      } finally {
1330        // ALWAYS runs whether success or error
1331        this.isSaving = false;
1332      }
1333    },
1334
1335    buildFullAddress() {
1336      const addr = this.groupInfo.address;
1337
1338      if (!addr || !addr.addressLine1 || !addr.city) {
1339        console.warn('Incomplete address data');
1340        return null;
1341      }
1342
1343      const parts = [
1344        addr.addressLine1,
1345        addr.addressLine2,
1346        addr.city,
1347        addr.region,
1348        addr.postalCode,
1349        addr.country,
1350      ].filter(Boolean);
1351
1352      const fullAddress = parts.join(', ');
1353      return fullAddress;
1354    },
1355
1356    async geocodeAddress(address) {
1357      return new Promise((resolve, reject) => {
1358        if (!window.google || !window.google.maps) {
1359          reject(new Error('Google Maps API is not available'));
1360          return;
1361        }
1362
1363        if (!address || address.trim() === '') {
1364          reject(new Error('Address is empty'));
1365          return;
1366        }
1367
1368        const geocoder = new google.maps.Geocoder();
1369
1370        geocoder.geocode({ address: address }, (results, status) => {
1371          if (status === google.maps.GeocoderStatus.OK && results && results.length > 0) {
1372            const location = results[0].geometry.location;
1373            resolve({
1374              latitude: location.lat(),
1375              longitude: location.lng(),
1376            });
1377          } else {
1378            reject(new Error(`Geocoding failed with status: ${status}`));
1379          }
1380        });
1381      });
1382    },
1383
1384    mapThumbnailIndex(groupTypeId) {
1385      const groupType = ROCK_GROUP_TYPE_OPTIONS.find((type) => type.id === groupTypeId);
1386      return groupType && groupType.thumbnailIndex !== undefined ? groupType.thumbnailIndex : 1;
1387    },
1388
1389    mapGenderForAPI(gender) {
1390      const genderMapping = {
1391        Men: 'Men',
1392        Women: 'Women',
1393        'Men & Women': 'Unspecified',
1394      };
1395      return genderMapping[gender] || gender;
1396    },
1397
1398    mapLifeStagesForAPI(lifeStages) {
1399      if (!Array.isArray(lifeStages)) return lifeStages;
1400
1401      // Handle special case for "Married & Singles"
1402      const result = [];
1403      lifeStages.forEach((stage) => {
1404        if (stage === 'Married & Single') {
1405          result.push('Married', 'Single');
1406        } else {
1407          result.push(stage);
1408        }
1409      });
1410      return result;
1411    },
1412
1413    combineTimeFields(time, ampm) {
1414      if (!time || !ampm) return this.groupInfo.schedule?.sequences?.[0]?.startTime;
1415
1416      const [hours, minutes] = time.split(':');
1417      let hour24 = parseInt(hours);
1418
1419      if (ampm === 'PM' && hour24 !== 12) hour24 += 12;
1420      if (ampm === 'AM' && hour24 === 12) hour24 = 0;
1421
1422      return `${hour24.toString().padStart(2, '0')}:${minutes}:00`;
1423    },
1424
1425    handleImageUpload() {
1426      this.showGroupImageModal(false);
1427      this.$emit('refetch-group', { message: 'Group Info has been updated' });
1428    },
1429
1430    showGroupImageModal(show) {
1431      this.isGroupImageModalVisible = show;
1432    },
1433
1434    stringJoin(array, separator = ', ') {
1435      if (!Array.isArray(array)) return '';
1436
1437      return array
1438        .filter((item) => typeof item === 'string' && item.trim() !== '') // optional: filter out empty or non-string entries
1439        .join(separator);
1440    },
1441
1442    getRegionList(country, retries = 2) {
1443      if (!country) {
1444        console.warn('No country provided to getRegionList');
1445        return;
1446      }
1447
1448      console.log('Fetching regions for country:', country);
1449      
1450      axios
1451        .get(`${this.healthyChurchApi}/api/v2/regions/by-country/${country}`)
1452        .then((response) => {
1453          const stateDropdown = $('#stateDropdown');
1454          const regionList = response.data.map((v) => ({
1455            id: v.shortName,
1456            text: v.longName,
1457          }));
1458
1459          stateDropdown.val(null).empty().select2('destroy');
1460          stateDropdown
1461            .select2({
1462              minimumResultsForSearch: 0,
1463              mobile: true,
1464              width: '100%',
1465              data: regionList,
1466            })
1467            .on('change', (e) => {
1468              this.groupInfo.region = e.target.value;
1469            });
1470
1471            // Set up keyboard navigation for the recreated region dropdown
1472            this.setupSingleDropdownNavigation(stateDropdown);
1473
1474          // Set the current region value after dropdown is populated
1475          this.$nextTick(() => {
1476            if (this.groupInfo.region) {
1477              stateDropdown.val(this.groupInfo.region).trigger('change.select2');
1478            }
1479          });
1480        })
1481        .catch((error) => {
1482          console.error('Error fetching region list:', error);
1483          
1484          // Retry logic
1485          if (retries > 0) {
1486            console.log(`Retrying getRegionList for ${country}, attempts remaining: ${retries}`);
1487            setTimeout(() => {
1488              this.getRegionList(country, retries - 1);
1489            }, 1000);
1490          }
1491        });
1492    },
1493  },
1494};

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