898 lines
35 KiB
PHP
898 lines
35 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Livewire\Profile;
|
|
|
|
use App\Exports\ProfileContactsExport;
|
|
use App\Exports\ProfileDataExport;
|
|
use App\Exports\ProfileMessagesExport;
|
|
use App\Exports\ProfileTagsExport;
|
|
use App\Exports\ProfileTransactionsExport;
|
|
use App\Helpers\ProfileAuthorizationHelper;
|
|
use App\Models\Account;
|
|
use App\Models\Tag;
|
|
use App\Models\Transaction;
|
|
use App\Models\User;
|
|
use App\Models\Organization;
|
|
use App\Models\Bank;
|
|
use Illuminate\Support\Facades\App;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Facades\URL;
|
|
use Livewire\Component;
|
|
use Namu\WireChat\Enums\ConversationType;
|
|
use Namu\WireChat\Models\Conversation;
|
|
use Namu\WireChat\Models\Message;
|
|
use Namu\WireChat\Models\Participant;
|
|
|
|
class ExportProfileData extends Component
|
|
{
|
|
/**
|
|
* Export all transactions from all accounts associated with the profile
|
|
*/
|
|
public function exportTransactions($type)
|
|
{
|
|
set_time_limit(0);
|
|
|
|
// Get active profile
|
|
$profileType = session('activeProfileType');
|
|
$profileId = session('activeProfileId');
|
|
$profile = $profileType::find($profileId);
|
|
|
|
if (!$profile) {
|
|
session()->flash('error', __('Profile not found'));
|
|
return;
|
|
}
|
|
|
|
// Validate authorization - ensure authenticated user owns/manages this profile
|
|
ProfileAuthorizationHelper::authorize($profile);
|
|
|
|
// Validate export type
|
|
if (!in_array($type, ['xlsx', 'ods', 'csv', 'json'])) {
|
|
session()->flash('error', __('Invalid export format'));
|
|
return;
|
|
}
|
|
|
|
// Get all account IDs for this profile
|
|
$accountIds = $profile->accounts()->pluck('id')->toArray();
|
|
|
|
if (empty($accountIds)) {
|
|
session()->flash('error', __('No accounts found for this profile'));
|
|
return;
|
|
}
|
|
|
|
// Get all transactions from all accounts
|
|
$transactions = Transaction::with([
|
|
'accountTo.accountable:id,name,full_name,profile_photo_path',
|
|
'accountFrom.accountable:id,name,full_name,profile_photo_path',
|
|
'transactionType:id,name'
|
|
])
|
|
->where(function ($query) use ($accountIds) {
|
|
$query->whereIn('to_account_id', $accountIds)
|
|
->orWhereIn('from_account_id', $accountIds);
|
|
})
|
|
->orderBy('created_at', 'desc')
|
|
->get();
|
|
|
|
// Transform data for export
|
|
$data = $transactions->map(function ($transaction) use ($accountIds) {
|
|
// Determine if this is debit or credit for this profile
|
|
$isDebit = in_array($transaction->from_account_id, $accountIds);
|
|
|
|
// Get the account and counter account
|
|
$account = $isDebit ? $transaction->accountFrom : $transaction->accountTo;
|
|
$counterAccount = $isDebit ? $transaction->accountTo : $transaction->accountFrom;
|
|
|
|
// Get relation (the other party)
|
|
$relation = $counterAccount->accountable;
|
|
|
|
return [
|
|
'trans_id' => $transaction->id,
|
|
'datetime' => $transaction->created_at->format('Y-m-d H:i:s'),
|
|
'amount' => $transaction->amount,
|
|
'c/d' => $isDebit ? 'Debit' : 'Credit',
|
|
'account_id' => $account->id,
|
|
'account_name' => $account->name,
|
|
'account_counter_id' => $counterAccount->id,
|
|
'account_counter_name' => $counterAccount->name,
|
|
'relation' => $relation->name ?? '',
|
|
'relation_full_name' => $relation->full_name ?? $relation->name ?? '',
|
|
'type' => $transaction->transactionType->name ?? '',
|
|
'description' => $transaction->description ?? '',
|
|
];
|
|
});
|
|
|
|
$this->dispatch('saved');
|
|
|
|
// Handle JSON export differently
|
|
if ($type === 'json') {
|
|
$fileName = 'profile-transactions.json';
|
|
|
|
// Transform data for JSON export
|
|
$jsonData = $data->map(function ($item) {
|
|
// Rename amount to amount_minutes and add amount_hours
|
|
$amountMinutes = $item['amount'];
|
|
unset($item['amount']);
|
|
$item['amount_minutes'] = $amountMinutes;
|
|
$item['amount_hours'] = round($amountMinutes / 60, 4);
|
|
|
|
return $item;
|
|
})->toArray();
|
|
|
|
$json = json_encode($jsonData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
|
|
return response()->streamDownload(function () use ($json) {
|
|
echo $json;
|
|
}, $fileName, [
|
|
'Content-Type' => 'application/json',
|
|
]);
|
|
}
|
|
|
|
return (new ProfileTransactionsExport($data))->download('profile-transactions.' . $type);
|
|
}
|
|
|
|
/**
|
|
* Export profile data
|
|
*/
|
|
public function exportProfileData($type)
|
|
{
|
|
// Get active profile
|
|
$profileType = session('activeProfileType');
|
|
$profileId = session('activeProfileId');
|
|
$profile = $profileType::find($profileId);
|
|
|
|
if (!$profile) {
|
|
session()->flash('error', __('Profile not found'));
|
|
return;
|
|
}
|
|
|
|
// Validate authorization - ensure authenticated user owns/manages this profile
|
|
ProfileAuthorizationHelper::authorize($profile);
|
|
|
|
// Validate export type
|
|
if (!in_array($type, ['xlsx', 'ods', 'csv', 'json'])) {
|
|
session()->flash('error', __('Invalid export format'));
|
|
return;
|
|
}
|
|
|
|
// Get location string if available
|
|
$location = '';
|
|
if ($profile->location) {
|
|
$locationParts = [];
|
|
if ($profile->location->district) {
|
|
$locationParts[] = $profile->location->district->name;
|
|
}
|
|
if ($profile->location->city) {
|
|
$locationParts[] = $profile->location->city->name;
|
|
}
|
|
if ($profile->location->division) {
|
|
$locationParts[] = $profile->location->division->name;
|
|
}
|
|
if ($profile->location->country) {
|
|
$locationParts[] = $profile->location->country->name;
|
|
}
|
|
$location = implode(', ', $locationParts);
|
|
}
|
|
|
|
// Get location first name
|
|
$locationFirstName = '';
|
|
$locationFirst = $profile->getLocationFirst();
|
|
if ($locationFirst) {
|
|
$locationFirstName = $locationFirst['name'] ?? '';
|
|
}
|
|
|
|
// Get social media accounts formatted as comma-separated string
|
|
$socials = [];
|
|
if ($profile->socials) {
|
|
foreach ($profile->socials as $social) {
|
|
$isBlueSky = $social->id == 3;
|
|
$isFullUrl = str_starts_with($social->pivot->user_on_social, 'https://');
|
|
|
|
if ($isBlueSky) {
|
|
$socials[] = '@' . $social->pivot->user_on_social;
|
|
} elseif ($isFullUrl) {
|
|
$socials[] = $social->pivot->user_on_social;
|
|
} elseif ($social->pivot->server_of_social) {
|
|
$socials[] = '@' . $social->pivot->user_on_social . '@' . $social->pivot->server_of_social;
|
|
} else {
|
|
$socials[] = '@' . $social->pivot->user_on_social;
|
|
}
|
|
}
|
|
}
|
|
$socialsString = implode("\n", $socials);
|
|
|
|
// Transform profile data to array
|
|
$data = collect([[
|
|
'name' => $profile->name,
|
|
'full_name' => $profile->full_name ?? '',
|
|
'email' => $profile->email ?? '',
|
|
'about' => strip_tags($profile->about ?? ''),
|
|
'about_short' => $profile->about_short ?? '',
|
|
'motivation' => strip_tags($profile->motivation ?? ''),
|
|
'website' => $profile->website ?? '',
|
|
'phone' => $profile->phone ?? '',
|
|
'phone_public' => $profile->phone_public ?? false,
|
|
'location' => $location,
|
|
'location_first' => $locationFirstName,
|
|
'social_media' => $socialsString,
|
|
'profile_photo_path' => $profile->profile_photo_path ?? '',
|
|
'lang_preference' => $profile->lang_preference ?? '',
|
|
'created_at' => $profile->created_at ? (is_object($profile->created_at) ? $profile->created_at->format('Y-m-d H:i:s') : $profile->created_at) : '',
|
|
'updated_at' => $profile->updated_at ? (is_object($profile->updated_at) ? $profile->updated_at->format('Y-m-d H:i:s') : $profile->updated_at) : '',
|
|
'last_login_at' => $profile->last_login_at ? (is_object($profile->last_login_at) ? $profile->last_login_at->format('Y-m-d H:i:s') : $profile->last_login_at) : '',
|
|
]]);
|
|
|
|
$profileTypeName = strtolower(class_basename($profileType));
|
|
|
|
$this->dispatch('saved');
|
|
|
|
// Handle JSON export differently
|
|
if ($type === 'json') {
|
|
$fileName = 'profile-data.json';
|
|
|
|
// Transform data for JSON export
|
|
$jsonData = $data->map(function ($item) {
|
|
// Remove location key
|
|
unset($item['location']);
|
|
|
|
// Replace newlines with commas in social_media
|
|
if (isset($item['social_media'])) {
|
|
$item['social_media'] = str_replace("\n", ', ', $item['social_media']);
|
|
}
|
|
|
|
// Reorder keys: put phone_visible_for_platform_users right after phone
|
|
$ordered = [];
|
|
foreach ($item as $key => $value) {
|
|
$ordered[$key] = $value;
|
|
if ($key === 'phone') {
|
|
// Rename and insert phone_public right after phone
|
|
$ordered['phone_visible_for_platform_users'] = $item['phone_public'];
|
|
}
|
|
}
|
|
// Remove the old phone_public key
|
|
unset($ordered['phone_public']);
|
|
|
|
return $ordered;
|
|
})->toArray();
|
|
|
|
$json = json_encode($jsonData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
|
|
return response()->streamDownload(function () use ($json) {
|
|
echo $json;
|
|
}, $fileName, [
|
|
'Content-Type' => 'application/json',
|
|
]);
|
|
}
|
|
|
|
return (new ProfileDataExport($data, $profileTypeName))->download('profile-data.' . $type);
|
|
}
|
|
|
|
/**
|
|
* Export all messages from conversations the profile participated in
|
|
*/
|
|
public function exportMessages($type)
|
|
{
|
|
set_time_limit(0);
|
|
|
|
// Get active profile
|
|
$profileType = session('activeProfileType');
|
|
$profileId = session('activeProfileId');
|
|
$profile = $profileType::find($profileId);
|
|
|
|
if (!$profile) {
|
|
session()->flash('error', __('Profile not found'));
|
|
return;
|
|
}
|
|
|
|
// Validate authorization - ensure authenticated user owns/manages this profile
|
|
ProfileAuthorizationHelper::authorize($profile);
|
|
|
|
// Validate export type
|
|
if (!in_array($type, ['xlsx', 'ods', 'csv', 'json'])) {
|
|
session()->flash('error', __('Invalid export format'));
|
|
return;
|
|
}
|
|
|
|
// Get all conversation IDs where the profile is a participant
|
|
$conversationIds = Participant::where('participantable_type', $profileType)
|
|
->where('participantable_id', $profileId)
|
|
->pluck('conversation_id');
|
|
|
|
if ($conversationIds->isEmpty()) {
|
|
session()->flash('error', __('No conversations found for this profile'));
|
|
return;
|
|
}
|
|
|
|
// Get all messages from those conversations with sender information
|
|
$messages = Message::with([
|
|
'conversation:id,type',
|
|
'sendable:id,name,full_name' // Load sender information
|
|
])
|
|
->whereIn('conversation_id', $conversationIds)
|
|
->orderBy('conversation_id', 'asc')
|
|
->orderBy('created_at', 'asc')
|
|
->get();
|
|
|
|
// Transform data for export
|
|
$data = $messages->map(function ($message) {
|
|
$conversationType = '';
|
|
if ($message->conversation && $message->conversation->type) {
|
|
$conversationType = is_object($message->conversation->type)
|
|
? $message->conversation->type->value
|
|
: $message->conversation->type;
|
|
}
|
|
|
|
// Get sender information
|
|
$senderName = '';
|
|
$senderType = '';
|
|
if ($message->sendable) {
|
|
$senderName = $message->sendable->full_name ?? $message->sendable->name ?? '';
|
|
$senderType = class_basename($message->sendable_type);
|
|
}
|
|
|
|
return [
|
|
'conversation_id' => $message->conversation_id,
|
|
'conversation_type' => $conversationType,
|
|
'id' => $message->id,
|
|
'created_at' => $message->created_at ? (is_object($message->created_at) ? $message->created_at->format('Y-m-d H:i:s') : $message->created_at) : '',
|
|
'sender_name' => $senderName,
|
|
'sender_type' => $senderType,
|
|
'body' => $message->body ?? '',
|
|
'reply_id' => $message->reply_id ?? '',
|
|
];
|
|
});
|
|
|
|
$this->dispatch('saved');
|
|
|
|
// Handle JSON export differently
|
|
if ($type === 'json') {
|
|
$fileName = 'profile-messages.json';
|
|
$json = json_encode($data->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
|
|
return response()->streamDownload(function () use ($json) {
|
|
echo $json;
|
|
}, $fileName, [
|
|
'Content-Type' => 'application/json',
|
|
]);
|
|
}
|
|
|
|
return (new ProfileMessagesExport($data))->download('profile-messages.' . $type);
|
|
}
|
|
|
|
/**
|
|
* Export all tags (skills) associated with the profile
|
|
* Tags are exported in the profile's language preference or fallback locale
|
|
*/
|
|
public function exportTags($type)
|
|
{
|
|
// Get active profile
|
|
$profileType = session('activeProfileType');
|
|
$profileId = session('activeProfileId');
|
|
$profile = $profileType::find($profileId);
|
|
|
|
if (!$profile) {
|
|
session()->flash('error', __('Profile not found'));
|
|
return;
|
|
}
|
|
|
|
// Validate authorization - ensure authenticated user owns/manages this profile
|
|
ProfileAuthorizationHelper::authorize($profile);
|
|
|
|
// Validate export type
|
|
if (!in_array($type, ['xlsx', 'ods', 'csv', 'json'])) {
|
|
session()->flash('error', __('Invalid export format'));
|
|
return;
|
|
}
|
|
|
|
// Determine which locale to use for tag translation
|
|
$locale = $profile->lang_preference ?? App::getLocale();
|
|
$fallbackLocale = App::getFallbackLocale();
|
|
|
|
// Get all tag IDs from the profile
|
|
$tagIds = $profile->tags->pluck('tag_id');
|
|
|
|
if ($tagIds->isEmpty()) {
|
|
session()->flash('error', __('No tags found for this profile'));
|
|
return;
|
|
}
|
|
|
|
// Translate tags to the profile's language preference (or fallback)
|
|
$translatedTags = collect((new Tag())->translateTagIdsWithContexts($tagIds));
|
|
|
|
// Transform data for export
|
|
$data = $translatedTags->map(function ($tag) {
|
|
return [
|
|
'tag_id' => $tag['tag_id'] ?? '',
|
|
'tag' => $tag['tag'] ?? '',
|
|
'category' => $tag['category'] ?? '',
|
|
'category_path' => $tag['category_path'] ?? '',
|
|
'locale' => $tag['locale'] ?? '',
|
|
];
|
|
});
|
|
|
|
$this->dispatch('saved');
|
|
|
|
// Handle JSON export differently
|
|
if ($type === 'json') {
|
|
$fileName = 'profile-tags.json';
|
|
$json = json_encode($data->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
|
|
return response()->streamDownload(function () use ($json) {
|
|
echo $json;
|
|
}, $fileName, [
|
|
'Content-Type' => 'application/json',
|
|
]);
|
|
}
|
|
|
|
return (new ProfileTagsExport($data))->download('profile-tags.' . $type);
|
|
}
|
|
|
|
/**
|
|
* Export all contacts associated with the profile
|
|
*/
|
|
public function exportContacts($type)
|
|
{
|
|
set_time_limit(0);
|
|
|
|
// Get active profile
|
|
$profileType = session('activeProfileType');
|
|
$profileId = session('activeProfileId');
|
|
$profile = $profileType::find($profileId);
|
|
|
|
if (!$profile) {
|
|
session()->flash('error', __('Profile not found'));
|
|
return;
|
|
}
|
|
|
|
// Validate authorization - ensure authenticated user owns/manages this profile
|
|
ProfileAuthorizationHelper::authorize($profile);
|
|
|
|
// Validate export type
|
|
if (!in_array($type, ['xlsx', 'ods', 'csv', 'json'])) {
|
|
session()->flash('error', __('Invalid export format'));
|
|
return;
|
|
}
|
|
|
|
// Initialize contacts collection
|
|
$contactsData = collect();
|
|
|
|
// Get the reacter_id and reactant_id for the active profile
|
|
$reacterId = $profile->love_reacter_id;
|
|
$reactantId = $profile->love_reactant_id;
|
|
|
|
// 1. Get profiles the active profile has reacted to (stars, bookmarks)
|
|
if ($reacterId) {
|
|
$reactedProfiles = $this->getReactedProfilesForExport($reacterId);
|
|
$contactsData = $contactsData->merge($reactedProfiles);
|
|
}
|
|
|
|
// 2. Get profiles that have transacted with the active profile
|
|
$transactionProfiles = $this->getTransactionProfilesForExport($profile);
|
|
$contactsData = $contactsData->merge($transactionProfiles);
|
|
|
|
// 3. Get profiles from private WireChat conversations
|
|
$conversationProfiles = $this->getConversationProfilesForExport($profile);
|
|
$contactsData = $contactsData->merge($conversationProfiles);
|
|
|
|
// Group by profile and merge interaction data
|
|
$contacts = $contactsData->groupBy('profile_key')->map(function ($group) {
|
|
$first = $group->first();
|
|
|
|
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'],
|
|
'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();
|
|
|
|
// Sort by last interaction (most recent first)
|
|
$contacts = $contacts->sortByDesc('last_interaction')->values();
|
|
|
|
$this->dispatch('saved');
|
|
|
|
// Handle JSON export differently
|
|
if ($type === 'json') {
|
|
$fileName = 'profile-contacts.json';
|
|
$json = json_encode($contacts->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
|
|
return response()->streamDownload(function () use ($json) {
|
|
echo $json;
|
|
}, $fileName, [
|
|
'Content-Type' => 'application/json',
|
|
]);
|
|
}
|
|
|
|
return (new ProfileContactsExport($contacts))->download('profile-contacts.' . $type);
|
|
}
|
|
|
|
/**
|
|
* Get profiles the active profile has reacted to.
|
|
*/
|
|
private function getReactedProfilesForExport($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->batchLoadLocationsForExport($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 getTransactionProfilesForExport($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->batchLoadLocationsForExport($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 getConversationProfilesForExport($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->batchLoadLocationsForExport($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.
|
|
*/
|
|
private function batchLoadLocationsForExport($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;
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
return view('livewire.profile.export-profile-data');
|
|
}
|
|
}
|