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.