Initial commit
This commit is contained in:
667
app/Http/Livewire/Contacts.php
Normal file
667
app/Http/Livewire/Contacts.php
Normal file
@@ -0,0 +1,667 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire;
|
||||
|
||||
use App\Models\Transaction;
|
||||
use App\Models\User;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Bank;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Namu\WireChat\Models\Conversation;
|
||||
use Namu\WireChat\Models\Participant;
|
||||
use Namu\WireChat\Enums\ConversationType;
|
||||
|
||||
class Contacts extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public $showSearchSection = false;
|
||||
public $search;
|
||||
public $searchInput = ''; // Temporary input for search field
|
||||
public $filterType = []; // Array of selected filter types
|
||||
public $filterTypeInput = []; // Temporary input for filter multiselect
|
||||
public $perPage = 15;
|
||||
public $sortField = 'last_interaction';
|
||||
public $sortAsc = false;
|
||||
|
||||
/**
|
||||
* Sort by a specific field.
|
||||
*/
|
||||
public function sortBy($field)
|
||||
{
|
||||
if ($this->sortField === $field) {
|
||||
$this->sortAsc = !$this->sortAsc;
|
||||
} else {
|
||||
$this->sortField = $field;
|
||||
$this->sortAsc = true;
|
||||
}
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
protected $rules = [
|
||||
'search' => 'nullable|string|min:2|max:100',
|
||||
];
|
||||
|
||||
protected $messages = [
|
||||
'search.min' => 'Search must be at least 2 characters.',
|
||||
'search.max' => 'Search cannot exceed 100 characters.',
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Apply search and filter when button is clicked.
|
||||
*/
|
||||
public function applySearch()
|
||||
{
|
||||
// Validate search input if provided
|
||||
if (!empty($this->searchInput) && strlen($this->searchInput) < 2) {
|
||||
$this->addError('search', 'Search must be at least 2 characters.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($this->searchInput) && strlen($this->searchInput) > 100) {
|
||||
$this->addError('search', 'Search cannot exceed 100 characters.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply the input values to the actual search properties
|
||||
$this->search = $this->searchInput;
|
||||
$this->filterType = $this->filterTypeInput;
|
||||
|
||||
// Reset to first page when searching
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all contacts (profiles) the active profile has interacted with.
|
||||
* Includes interactions from:
|
||||
* - Laravel-love reactions (bookmarks, stars)
|
||||
* - Transactions (sent to or received from)
|
||||
* - WireChat private conversations
|
||||
*
|
||||
* @return \Illuminate\Pagination\LengthAwarePaginator
|
||||
*/
|
||||
public function getContacts()
|
||||
{
|
||||
$activeProfile = getActiveProfile();
|
||||
|
||||
if (!$activeProfile) {
|
||||
// Return empty paginator instead of null
|
||||
return new \Illuminate\Pagination\LengthAwarePaginator(
|
||||
collect([]),
|
||||
0,
|
||||
$this->perPage,
|
||||
1,
|
||||
['path' => request()->url(), 'pageName' => 'page']
|
||||
);
|
||||
}
|
||||
|
||||
// CRITICAL SECURITY: Validate user has ownership/access to this profile
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($activeProfile);
|
||||
|
||||
// Initialize contacts collection
|
||||
$contactsData = collect();
|
||||
|
||||
// Get the reacter_id and reactant_id for the active profile
|
||||
$reacterId = $activeProfile->love_reacter_id;
|
||||
$reactantId = $activeProfile->love_reactant_id;
|
||||
|
||||
// If no filters selected, show all
|
||||
$showAll = empty($this->filterType);
|
||||
|
||||
// 1. Get profiles the active profile has reacted to (stars, bookmarks)
|
||||
if ($showAll || in_array('stars', $this->filterType) || in_array('bookmarks', $this->filterType)) {
|
||||
if ($reacterId) {
|
||||
$reactedProfiles = $this->getReactedProfiles($reacterId);
|
||||
|
||||
// Filter by specific reaction types if selected
|
||||
if (!$showAll && (in_array('stars', $this->filterType) || in_array('bookmarks', $this->filterType))) {
|
||||
$reactedProfiles = $reactedProfiles->filter(function ($profile) {
|
||||
if (in_array('stars', $this->filterType) && $profile['interaction_type'] === 'star') {
|
||||
return true;
|
||||
}
|
||||
if (in_array('bookmarks', $this->filterType) && $profile['interaction_type'] === 'bookmark') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
$contactsData = $contactsData->merge($reactedProfiles);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Get profiles that have transacted with the active profile
|
||||
if ($showAll || in_array('transactions', $this->filterType)) {
|
||||
$transactionProfiles = $this->getTransactionProfiles($activeProfile);
|
||||
$contactsData = $contactsData->merge($transactionProfiles);
|
||||
}
|
||||
|
||||
// 3. Get profiles from private WireChat conversations
|
||||
if ($showAll || in_array('conversations', $this->filterType)) {
|
||||
$conversationProfiles = $this->getConversationProfiles($activeProfile);
|
||||
$contactsData = $contactsData->merge($conversationProfiles);
|
||||
}
|
||||
|
||||
// Group by profile and merge interaction data
|
||||
$contacts = $contactsData->groupBy('profile_key')->map(function ($group) {
|
||||
$first = $group->first();
|
||||
|
||||
// Construct profile path like in SingleTransactionTable
|
||||
$profileTypeLower = strtolower($first['profile_type_name']);
|
||||
$profilePath = URL::to('/') . '/' . __($profileTypeLower) . '/' . $first['profile_id'];
|
||||
|
||||
return [
|
||||
'profile_id' => $first['profile_id'],
|
||||
'profile_type' => $first['profile_type'],
|
||||
'profile_type_name' => $first['profile_type_name'],
|
||||
'name' => $first['name'],
|
||||
'full_name' => $first['full_name'],
|
||||
'location' => $first['location'],
|
||||
'profile_photo' => $first['profile_photo'],
|
||||
'profile_path' => $profilePath,
|
||||
'has_star' => $group->contains('interaction_type', 'star'),
|
||||
'has_bookmark' => $group->contains('interaction_type', 'bookmark'),
|
||||
'has_transaction' => $group->contains('interaction_type', 'transaction'),
|
||||
'has_conversation' => $group->contains('interaction_type', 'conversation'),
|
||||
'last_interaction' => $group->max('last_interaction'),
|
||||
'star_count' => $group->where('interaction_type', 'star')->sum('count'),
|
||||
'bookmark_count' => $group->where('interaction_type', 'bookmark')->sum('count'),
|
||||
'transaction_count' => $group->where('interaction_type', 'transaction')->sum('count'),
|
||||
'message_count' => $group->where('interaction_type', 'conversation')->sum('count'),
|
||||
];
|
||||
})->values();
|
||||
|
||||
// Apply search filter
|
||||
if (!empty($this->search) && strlen(trim($this->search)) >= 2) {
|
||||
$search = strtolower(trim($this->search));
|
||||
$contacts = $contacts->filter(function ($contact) use ($search) {
|
||||
$name = strtolower($contact['name'] ?? '');
|
||||
$fullName = strtolower($contact['full_name'] ?? '');
|
||||
$location = strtolower($contact['location'] ?? '');
|
||||
|
||||
return str_contains($name, $search) ||
|
||||
str_contains($fullName, $search) ||
|
||||
str_contains($location, $search);
|
||||
})->values();
|
||||
}
|
||||
|
||||
// Sort contacts
|
||||
$contacts = $this->sortContacts($contacts);
|
||||
|
||||
// Paginate manually
|
||||
$currentPage = $this->paginators['page'] ?? 1;
|
||||
$total = $contacts->count();
|
||||
$items = $contacts->forPage($currentPage, $this->perPage);
|
||||
|
||||
return new \Illuminate\Pagination\LengthAwarePaginator(
|
||||
$items,
|
||||
$total,
|
||||
$this->perPage,
|
||||
$currentPage,
|
||||
['path' => request()->url(), 'pageName' => 'page']
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get profiles the active profile has reacted to.
|
||||
*/
|
||||
private function getReactedProfiles($reacterId)
|
||||
{
|
||||
// Get all reactions by this reacter, grouped by reactant type
|
||||
$reactions = DB::table('love_reactions')
|
||||
->join('love_reactants', 'love_reactions.reactant_id', '=', 'love_reactants.id')
|
||||
->where('love_reactions.reacter_id', $reacterId)
|
||||
->select(
|
||||
'love_reactants.type as reactant_type',
|
||||
DB::raw('CAST(SUBSTRING_INDEX(love_reactants.type, "\\\\", -1) AS CHAR) as reactant_model')
|
||||
)
|
||||
->groupBy('love_reactants.type')
|
||||
->get();
|
||||
|
||||
$profiles = collect();
|
||||
|
||||
foreach ($reactions as $reaction) {
|
||||
// Only process User, Organization, and Bank models
|
||||
if (!in_array($reaction->reactant_model, ['User', 'Organization', 'Bank'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$modelClass = "App\\Models\\{$reaction->reactant_model}";
|
||||
|
||||
// Get all profiles of this type that were reacted to, with reaction type breakdown
|
||||
$reactedToProfiles = DB::table('love_reactions')
|
||||
->join('love_reactants', 'love_reactions.reactant_id', '=', 'love_reactants.id')
|
||||
->join(
|
||||
DB::raw("(SELECT id, love_reactant_id, name,
|
||||
full_name,
|
||||
profile_photo_path
|
||||
FROM " . strtolower($reaction->reactant_model) . "s) as profiles"),
|
||||
'love_reactants.id',
|
||||
'=',
|
||||
'profiles.love_reactant_id'
|
||||
)
|
||||
->where('love_reactions.reacter_id', $reacterId)
|
||||
->where('love_reactants.type', $reaction->reactant_type)
|
||||
->select(
|
||||
'profiles.id as profile_id',
|
||||
'profiles.name',
|
||||
'profiles.full_name',
|
||||
'profiles.profile_photo_path',
|
||||
DB::raw("'{$modelClass}' as profile_type"),
|
||||
DB::raw("'{$reaction->reactant_model}' as profile_type_name"),
|
||||
'love_reactions.reaction_type_id',
|
||||
DB::raw('MAX(love_reactions.created_at) as last_interaction'),
|
||||
DB::raw('COUNT(*) as count')
|
||||
)
|
||||
->groupBy('profiles.id', 'profiles.name', 'profiles.full_name', 'profiles.profile_photo_path', 'love_reactions.reaction_type_id')
|
||||
->get();
|
||||
|
||||
// Batch load locations for all profiles of this type
|
||||
$profileIds = $reactedToProfiles->pluck('profile_id');
|
||||
$locations = $this->batchLoadLocations($modelClass, $profileIds);
|
||||
|
||||
foreach ($reactedToProfiles as $profile) {
|
||||
// Get location from batch-loaded data
|
||||
$location = $locations[$profile->profile_id] ?? '';
|
||||
|
||||
// Determine reaction type (1 = Star, 2 = Bookmark)
|
||||
$interactionType = $profile->reaction_type_id == 1 ? 'star' : ($profile->reaction_type_id == 2 ? 'bookmark' : 'reaction');
|
||||
|
||||
$profiles->push([
|
||||
'profile_key' => $modelClass . '_' . $profile->profile_id,
|
||||
'profile_id' => $profile->profile_id,
|
||||
'profile_type' => $profile->profile_type,
|
||||
'profile_type_name' => $profile->profile_type_name,
|
||||
'name' => $profile->name,
|
||||
'full_name' => $profile->full_name,
|
||||
'location' => $location,
|
||||
'profile_photo' => $profile->profile_photo_path,
|
||||
'interaction_type' => $interactionType,
|
||||
'last_interaction' => $profile->last_interaction,
|
||||
'count' => $profile->count,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $profiles;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get profiles that have transacted with the active profile.
|
||||
*/
|
||||
private function getTransactionProfiles($activeProfile)
|
||||
{
|
||||
// Get all accounts belonging to the active profile
|
||||
$accountIds = DB::table('accounts')
|
||||
->where('accountable_type', get_class($activeProfile))
|
||||
->where('accountable_id', $activeProfile->id)
|
||||
->pluck('id');
|
||||
|
||||
if ($accountIds->isEmpty()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
// Get all transactions involving these accounts
|
||||
$transactions = DB::table('transactions')
|
||||
->whereIn('from_account_id', $accountIds)
|
||||
->orWhereIn('to_account_id', $accountIds)
|
||||
->select(
|
||||
'from_account_id',
|
||||
'to_account_id',
|
||||
DB::raw('MAX(created_at) as last_interaction'),
|
||||
DB::raw('COUNT(*) as count')
|
||||
)
|
||||
->groupBy('from_account_id', 'to_account_id')
|
||||
->get();
|
||||
|
||||
// Group counter accounts by type for batch loading
|
||||
$counterAccountsByType = collect();
|
||||
|
||||
foreach ($transactions as $transaction) {
|
||||
// Determine the counter account (the other party in the transaction)
|
||||
$counterAccountId = null;
|
||||
if ($accountIds->contains($transaction->from_account_id) && !$accountIds->contains($transaction->to_account_id)) {
|
||||
$counterAccountId = $transaction->to_account_id;
|
||||
} elseif ($accountIds->contains($transaction->to_account_id) && !$accountIds->contains($transaction->from_account_id)) {
|
||||
$counterAccountId = $transaction->from_account_id;
|
||||
}
|
||||
|
||||
if ($counterAccountId) {
|
||||
$transaction->counter_account_id = $counterAccountId;
|
||||
}
|
||||
}
|
||||
|
||||
// Get all counter account details in one query
|
||||
$counterAccountIds = $transactions->pluck('counter_account_id')->filter()->unique();
|
||||
$accounts = DB::table('accounts')
|
||||
->whereIn('id', $counterAccountIds)
|
||||
->select('id', 'accountable_type', 'accountable_id')
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
// Group profile IDs by type
|
||||
$profileIdsByType = [];
|
||||
foreach ($accounts as $account) {
|
||||
$profileTypeName = class_basename($account->accountable_type);
|
||||
if (!isset($profileIdsByType[$profileTypeName])) {
|
||||
$profileIdsByType[$profileTypeName] = [];
|
||||
}
|
||||
$profileIdsByType[$profileTypeName][] = $account->accountable_id;
|
||||
}
|
||||
|
||||
// Batch load profile data and locations for each type
|
||||
$profileDataByType = [];
|
||||
$locationsByType = [];
|
||||
foreach ($profileIdsByType as $typeName => $ids) {
|
||||
$tableName = strtolower($typeName) . 's';
|
||||
$modelClass = "App\\Models\\{$typeName}";
|
||||
|
||||
// Load profile data
|
||||
$profileDataByType[$typeName] = DB::table($tableName)
|
||||
->whereIn('id', $ids)
|
||||
->select('id', 'name', 'full_name', 'profile_photo_path')
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
// Batch load locations
|
||||
$locationsByType[$typeName] = $this->batchLoadLocations($modelClass, $ids);
|
||||
}
|
||||
|
||||
// Build final profiles collection
|
||||
$profiles = collect();
|
||||
foreach ($transactions as $transaction) {
|
||||
if (!isset($transaction->counter_account_id)) {
|
||||
continue; // Skip self-transactions
|
||||
}
|
||||
|
||||
$account = $accounts->get($transaction->counter_account_id);
|
||||
if (!$account) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$profileModel = $account->accountable_type;
|
||||
$profileId = $account->accountable_id;
|
||||
$profileTypeName = class_basename($profileModel);
|
||||
|
||||
$profile = $profileDataByType[$profileTypeName][$profileId] ?? null;
|
||||
if (!$profile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$location = $locationsByType[$profileTypeName][$profileId] ?? '';
|
||||
$profileKey = $profileModel . '_' . $profileId;
|
||||
|
||||
$profiles->push([
|
||||
'profile_key' => $profileKey,
|
||||
'profile_id' => $profileId,
|
||||
'profile_type' => $profileModel,
|
||||
'profile_type_name' => $profileTypeName,
|
||||
'name' => $profile->name,
|
||||
'full_name' => $profile->full_name,
|
||||
'location' => $location,
|
||||
'profile_photo' => $profile->profile_photo_path,
|
||||
'interaction_type' => 'transaction',
|
||||
'last_interaction' => $transaction->last_interaction,
|
||||
'count' => $transaction->count,
|
||||
]);
|
||||
}
|
||||
|
||||
return $profiles;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get profiles from private WireChat conversations.
|
||||
*/
|
||||
private function getConversationProfiles($activeProfile)
|
||||
{
|
||||
// Get all private conversations the active profile is participating in
|
||||
$participantType = get_class($activeProfile);
|
||||
$participantId = $activeProfile->id;
|
||||
|
||||
// Get participant record for active profile
|
||||
$myParticipants = DB::table('wirechat_participants')
|
||||
->join('wirechat_conversations', 'wirechat_participants.conversation_id', '=', 'wirechat_conversations.id')
|
||||
->where('wirechat_participants.participantable_type', $participantType)
|
||||
->where('wirechat_participants.participantable_id', $participantId)
|
||||
->where('wirechat_conversations.type', ConversationType::PRIVATE->value)
|
||||
->whereNull('wirechat_participants.deleted_at')
|
||||
->select(
|
||||
'wirechat_participants.conversation_id',
|
||||
'wirechat_participants.last_active_at'
|
||||
)
|
||||
->get();
|
||||
|
||||
if ($myParticipants->isEmpty()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$conversationIds = $myParticipants->pluck('conversation_id');
|
||||
|
||||
// Get all other participants in one query
|
||||
$otherParticipants = DB::table('wirechat_participants')
|
||||
->whereIn('conversation_id', $conversationIds)
|
||||
->where(function ($query) use ($participantType, $participantId) {
|
||||
$query->where('participantable_type', '!=', $participantType)
|
||||
->orWhere('participantable_id', '!=', $participantId);
|
||||
})
|
||||
->whereNull('deleted_at')
|
||||
->get()
|
||||
->keyBy('conversation_id');
|
||||
|
||||
// Get message counts for all conversations in one query
|
||||
$messageCounts = DB::table('wirechat_messages')
|
||||
->whereIn('conversation_id', $conversationIds)
|
||||
->whereNull('deleted_at')
|
||||
->select(
|
||||
'conversation_id',
|
||||
DB::raw('COUNT(DISTINCT DATE(created_at)) as day_count')
|
||||
)
|
||||
->groupBy('conversation_id')
|
||||
->get()
|
||||
->keyBy('conversation_id');
|
||||
|
||||
// Get last messages for all conversations in one query
|
||||
$lastMessages = DB::table('wirechat_messages as wm1')
|
||||
->whereIn('wm1.conversation_id', $conversationIds)
|
||||
->whereNull('wm1.deleted_at')
|
||||
->whereRaw('wm1.created_at = (SELECT MAX(wm2.created_at) FROM wirechat_messages wm2 WHERE wm2.conversation_id = wm1.conversation_id AND wm2.deleted_at IS NULL)')
|
||||
->select('wm1.conversation_id', 'wm1.created_at')
|
||||
->get()
|
||||
->keyBy('conversation_id');
|
||||
|
||||
// Group profile IDs by type
|
||||
$profileIdsByType = [];
|
||||
foreach ($otherParticipants as $participant) {
|
||||
$profileTypeName = class_basename($participant->participantable_type);
|
||||
if (!isset($profileIdsByType[$profileTypeName])) {
|
||||
$profileIdsByType[$profileTypeName] = [];
|
||||
}
|
||||
$profileIdsByType[$profileTypeName][] = $participant->participantable_id;
|
||||
}
|
||||
|
||||
// Batch load profile data and locations for each type
|
||||
$profileDataByType = [];
|
||||
$locationsByType = [];
|
||||
foreach ($profileIdsByType as $typeName => $ids) {
|
||||
$tableName = strtolower($typeName) . 's';
|
||||
$modelClass = "App\\Models\\{$typeName}";
|
||||
|
||||
// Load profile data
|
||||
$profileDataByType[$typeName] = DB::table($tableName)
|
||||
->whereIn('id', $ids)
|
||||
->select('id', 'name', 'full_name', 'profile_photo_path')
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
// Batch load locations
|
||||
$locationsByType[$typeName] = $this->batchLoadLocations($modelClass, $ids);
|
||||
}
|
||||
|
||||
// Build final profiles collection
|
||||
$profiles = collect();
|
||||
foreach ($myParticipants as $myParticipant) {
|
||||
$otherParticipant = $otherParticipants->get($myParticipant->conversation_id);
|
||||
if (!$otherParticipant) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$profileModel = $otherParticipant->participantable_type;
|
||||
$profileId = $otherParticipant->participantable_id;
|
||||
$profileTypeName = class_basename($profileModel);
|
||||
|
||||
$profile = $profileDataByType[$profileTypeName][$profileId] ?? null;
|
||||
if (!$profile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$messageCount = $messageCounts->get($myParticipant->conversation_id)->day_count ?? 0;
|
||||
$lastMessage = $lastMessages->get($myParticipant->conversation_id);
|
||||
$location = $locationsByType[$profileTypeName][$profileId] ?? '';
|
||||
$profileKey = $profileModel . '_' . $profileId;
|
||||
|
||||
$profiles->push([
|
||||
'profile_key' => $profileKey,
|
||||
'profile_id' => $profileId,
|
||||
'profile_type' => $profileModel,
|
||||
'profile_type_name' => $profileTypeName,
|
||||
'name' => $profile->name,
|
||||
'full_name' => $profile->full_name,
|
||||
'location' => $location,
|
||||
'profile_photo' => $profile->profile_photo_path,
|
||||
'interaction_type' => 'conversation',
|
||||
'last_interaction' => $lastMessage ? $lastMessage->created_at : $myParticipant->last_active_at,
|
||||
'count' => $messageCount,
|
||||
]);
|
||||
}
|
||||
|
||||
return $profiles;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Batch load locations for multiple profiles of the same type.
|
||||
* This replaces the N+1 query problem in getProfileLocation().
|
||||
*
|
||||
* @param string $modelClass The model class (e.g., 'App\Models\User')
|
||||
* @param array|\Illuminate\Support\Collection $profileIds Array of profile IDs
|
||||
* @return array Associative array of profile_id => location_name
|
||||
*/
|
||||
private function batchLoadLocations($modelClass, $profileIds)
|
||||
{
|
||||
if (empty($profileIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Ensure it's an array
|
||||
if ($profileIds instanceof \Illuminate\Support\Collection) {
|
||||
$profileIds = $profileIds->toArray();
|
||||
}
|
||||
|
||||
// Load all profiles with their location relationships
|
||||
$profiles = $modelClass::with([
|
||||
'locations.city.translations',
|
||||
'locations.district.translations',
|
||||
'locations.division.translations',
|
||||
'locations.country.translations'
|
||||
])
|
||||
->whereIn('id', $profileIds)
|
||||
->get();
|
||||
|
||||
// Build location map
|
||||
$locationMap = [];
|
||||
foreach ($profiles as $profile) {
|
||||
if (method_exists($profile, 'getLocationFirst')) {
|
||||
$locationData = $profile->getLocationFirst(false);
|
||||
$locationMap[$profile->id] = $locationData['name'] ?? $locationData['name_short'] ?? '';
|
||||
} else {
|
||||
$locationMap[$profile->id] = '';
|
||||
}
|
||||
}
|
||||
|
||||
return $locationMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get location for a profile (deprecated, use batchLoadLocations instead).
|
||||
* Kept for backwards compatibility.
|
||||
*/
|
||||
private function getProfileLocation($modelClass, $profileId)
|
||||
{
|
||||
$locations = $this->batchLoadLocations($modelClass, [$profileId]);
|
||||
return $locations[$profileId] ?? '';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sort contacts based on sort field and direction.
|
||||
*/
|
||||
private function sortContacts($contacts)
|
||||
{
|
||||
$sortField = $this->sortField;
|
||||
$sortAsc = $this->sortAsc;
|
||||
|
||||
return $contacts->sort(function ($a, $b) use ($sortField, $sortAsc) {
|
||||
$aVal = $a[$sortField] ?? '';
|
||||
$bVal = $b[$sortField] ?? '';
|
||||
|
||||
if ($sortField === 'last_interaction') {
|
||||
$comparison = strtotime($bVal) <=> strtotime($aVal); // Default: most recent first
|
||||
return $sortAsc ? -$comparison : $comparison;
|
||||
}
|
||||
|
||||
// For count fields, use numeric comparison
|
||||
if (in_array($sortField, ['transaction_count', 'message_count'])) {
|
||||
$comparison = ($aVal ?? 0) <=> ($bVal ?? 0);
|
||||
return $sortAsc ? $comparison : -$comparison;
|
||||
}
|
||||
|
||||
// For boolean fields
|
||||
if (in_array($sortField, ['has_star', 'has_bookmark'])) {
|
||||
$comparison = ($aVal ? 1 : 0) <=> ($bVal ? 1 : 0);
|
||||
return $sortAsc ? $comparison : -$comparison;
|
||||
}
|
||||
|
||||
// String comparison for name, location, etc.
|
||||
$comparison = strcasecmp($aVal, $bVal);
|
||||
return $sortAsc ? $comparison : -$comparison;
|
||||
})->values();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reset search and filters.
|
||||
*/
|
||||
public function resetSearch()
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->showSearchSection = false;
|
||||
$this->search = null;
|
||||
$this->searchInput = '';
|
||||
$this->filterType = [];
|
||||
$this->filterTypeInput = [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Scroll to top when page changes.
|
||||
*/
|
||||
public function updatedPage()
|
||||
{
|
||||
$this->dispatch('scroll-to-top');
|
||||
}
|
||||
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.contacts.show', [
|
||||
'contacts' => $this->getContacts(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user