related = $related; $this->random = $random; $this->maxCards = $maxCards; $carouselCfg = timebank_config('calls.carousel', []); $this->maxCards = (int) ($carouselCfg['max_cards'] ?? ($maxCards ?: 12)); $locale = App::getLocale(); // --- Resolve active profile and its location --- $profile = getActiveProfile(); if ($profile) { \App\Helpers\ProfileAuthorizationHelper::authorize($profile); } $location = $profile?->locations ? $profile->locations()->first() : null; $profileCityId = $location ? ($location->city_id ?? $location->city?->id) : null; $profileDivisionId = $location ? ($location->division_id ?? $location->division?->id) : null; $profileCountryId = $location ? ($location->country_id ?? $location->country?->id) : null; // Expand location IDs based on $related flag (same sibling logic as EventCardFull) $locationCityIds = []; $locationDivisionIds = []; $locationCountryIds = []; if ($location) { if (!$location->division && !$location->city) { $country = Location::find($location->id)->country; $locationCountryIds = $related ? Country::pluck('id')->toArray() : ($country ? [$country->id] : []); } elseif ($location->division && !$location->city) { $divisionId = Location::find($location->id)->division->id; $locationDivisionIds = $related ? Division::find($divisionId)->parent->divisions->pluck('id')->toArray() : [$divisionId]; } elseif ($location->city) { $cityId = Location::find($location->id)->city->id; $locationCityIds = $related ? City::find($cityId)->parent->cities->pluck('id')->toArray() : [$cityId]; } } // --- Base query — safety exclusions are always hardcoded --- $query = Call::with([ 'tag.contexts.category.translations', 'tag.contexts.category.ancestors.translations', 'translations', 'location.city.translations', 'location.country.translations', 'callable.locations.city.translations', 'callable.locations.division.translations', 'callable.locations.country.translations', 'callable.loveReactant.reactionCounters', 'loveReactant.reactionCounters', ]) ->whereNull('deleted_at') ->where('is_paused', false) ->where('is_suppressed', false) ->where(fn ($q) => $q->whereNull('till')->orWhere('till', '>=', now())); // Enforce is_public for guests or when config requires it if (!Auth::check() || ($carouselCfg['exclude_non_public'] ?? true)) { $query->where('is_public', true); } // Configurable: exclude the active profile's own calls if ($profile && ($carouselCfg['exclude_own_calls'] ?? true)) { $query->where(function ($q) use ($profile) { $q->where('callable_type', '!=', get_class($profile)) ->orWhere('callable_id', '!=', $profile->id); }); } // --- Locality filter --- $includeUnknown = $carouselCfg['include_unknown_location'] ?? true; $includeDivision = $carouselCfg['include_same_division'] ?? true; $includeCountry = $carouselCfg['include_same_country'] ?? true; $hasLocalityFilter = $locationCityIds || $locationDivisionIds || $locationCountryIds || ($includeDivision && $locationDivisionIds) || ($includeCountry && $locationCountryIds) || $includeUnknown; if ($hasLocalityFilter) { $query->where(function ($q) use ( $locationCityIds, $locationDivisionIds, $locationCountryIds, $includeDivision, $includeCountry, $includeUnknown ) { // Always include calls matching the profile's city if ($locationCityIds) { $q->orWhereHas('location', fn ($lq) => $lq->whereIn('city_id', $locationCityIds)); } if ($includeDivision && $locationDivisionIds) { $q->orWhereHas('location', fn ($lq) => $lq->whereIn('division_id', $locationDivisionIds)); } if ($includeCountry && $locationCountryIds) { $q->orWhereHas('location', fn ($lq) => $lq->whereIn('country_id', $locationCountryIds)); } if ($includeUnknown) { $q->orWhereHas('location', fn ($lq) => $lq->where('country_id', self::UNKNOWN_COUNTRY_ID)); } }); } // Fetch pool for scoring $isAdmin = getActiveProfileType() === 'Admin'; $this->showScore = (bool) ($carouselCfg['show_score'] ?? false) || ($isAdmin && (bool) ($carouselCfg['show_score_for_admins'] ?? true)); $poolSize = $this->maxCards * max(1, (int) ($carouselCfg['pool_multiplier'] ?? 5)); $calls = $query->limit($poolSize)->get(); // --- Score, sort, take top $maxCards --- $scorer = new CallCarouselScorer( $carouselCfg, $profileCityId, $profileDivisionId, $profileCountryId ); $this->calls = $calls->map(function (Call $call) use ($locale, $scorer) { $translation = $call->translations->firstWhere('locale', $locale) ?? $call->translations->first(); $tag = $call->tag; $tagContext = $tag?->contexts->first(); $tagCategory = $tagContext?->category; $tagColor = $tagCategory?->relatedColor ?? 'gray'; $tagName = $tag?->translation?->name ?? $tag?->name; $locationStr = null; if ($call->location) { $loc = $call->location; $parts = []; if ($loc->city) { $cityName = optional($loc->city->translations->first())->name; if ($cityName) $parts[] = $cityName; } if ($loc->country) { if ($loc->country->code === 'XX') { $parts[] = __('Location not specified'); } elseif ($loc->country->code) { $parts[] = strtoupper($loc->country->code); } } $locationStr = $parts ? implode(', ', $parts) : null; } $tagCategories = []; if ($tagCategory) { $ancestors = $tagCategory->ancestorsAndSelf()->get()->reverse(); foreach ($ancestors as $cat) { $catName = $cat->translations->firstWhere('locale', $locale)?->name ?? $cat->translations->first()?->name ?? ''; if ($catName) { $tagCategories[] = [ 'name' => $catName, 'color' => $cat->relatedColor ?? 'gray', ]; } } } $score = $scorer->score($call); // Add random jitter when $random=true to vary order while preserving scoring preference if ($this->random) { $score *= (random_int(85, 115) / 100); } return [ 'id' => $call->id, 'model' => Call::class, 'title' => $tagName ?? '', 'excerpt' => $translation?->content ?? '', 'photo' => $call->callable?->profile_photo_url ?? '', 'location' => $locationStr, 'tag_color' => $tagColor, 'tag_categories' => $tagCategories, 'callable_name' => $call->callable?->name ?? '', 'callable_location' => ProfileCalls::buildCallableLocation($call->callable), 'till' => $call->till, 'expiry_badge_text' => ProfileCalls::buildExpiryBadgeText($call->till), 'like_count' => $call->loveReactant?->reactionCounters ->firstWhere('reaction_type_id', 3)?->count ?? 0, 'score' => $score, ]; }) ->sortByDesc('score') ->take($this->maxCards) ->values() ->toArray(); } public function render() { return view('livewire.main-page.call-card-carousel'); } }