227 lines
9.5 KiB
PHP
227 lines
9.5 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Livewire\MainPage;
|
|
|
|
use App\Http\Livewire\Calls\CallCarouselScorer;
|
|
use App\Http\Livewire\Calls\ProfileCalls;
|
|
use App\Models\Call;
|
|
use App\Models\Locations\City;
|
|
use App\Models\Locations\Country;
|
|
use App\Models\Locations\Division;
|
|
use App\Models\Locations\Location;
|
|
use Illuminate\Support\Facades\App;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Livewire\Component;
|
|
|
|
class CallCardCarousel extends Component
|
|
{
|
|
public array $calls = [];
|
|
public bool $related = true;
|
|
public bool $random = false;
|
|
public int $maxCards = 0;
|
|
public bool $showScore = false;
|
|
private const UNKNOWN_COUNTRY_ID = 10;
|
|
|
|
public function mount(bool $related, bool $random = false, int $maxCards = 0): void
|
|
{
|
|
$this->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');
|
|
}
|
|
}
|