Initial commit
This commit is contained in:
905
app/Http/Livewire/Posts/BackupRestore.php
Normal file
905
app/Http/Livewire/Posts/BackupRestore.php
Normal file
@@ -0,0 +1,905 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Posts;
|
||||
|
||||
use App\Http\Livewire\Traits\RequiresAdminAuthorization;
|
||||
use App\Models\Category;
|
||||
use App\Models\Locations\CityLocale;
|
||||
use App\Models\Locations\CountryLocale;
|
||||
use App\Models\Locations\DistrictLocale;
|
||||
use App\Models\Locations\DivisionLocale;
|
||||
use App\Models\Locations\Location;
|
||||
use App\Models\Meeting;
|
||||
use App\Models\Post;
|
||||
use App\Models\PostTranslation;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Laravel\Scout\Jobs\MakeSearchable;
|
||||
use Livewire\Component;
|
||||
use WireUi\Traits\WireUiActions;
|
||||
use ZipArchive;
|
||||
|
||||
class BackupRestore extends Component
|
||||
{
|
||||
use WireUiActions;
|
||||
use RequiresAdminAuthorization;
|
||||
|
||||
public bool $showRestoreModal = false;
|
||||
|
||||
// Chunked upload state
|
||||
public ?string $uploadId = null;
|
||||
public ?string $uploadedFilePath = null;
|
||||
public ?string $selectedFileName = null;
|
||||
|
||||
// Optional: show "Backup selected" button (requires parent component to provide selection)
|
||||
public bool $showBackupSelected = false;
|
||||
public array $selectedTranslationIds = [];
|
||||
|
||||
// Restore state
|
||||
public array $restorePreview = [];
|
||||
public array $restorePostList = []; // Lightweight summaries for selection display
|
||||
public array $selectedPostIndices = []; // Selected post indices from backup
|
||||
public bool $selectAllPosts = true;
|
||||
public bool $isRestoring = false;
|
||||
public string $duplicateAction = 'skip'; // skip, overwrite
|
||||
public array $restoreStats = [];
|
||||
|
||||
protected $listeners = [
|
||||
'refreshComponent' => '$refresh',
|
||||
'updateSelectedTranslationIds' => 'updateSelectedTranslationIds',
|
||||
];
|
||||
|
||||
public function mount(bool $showBackupSelected = false)
|
||||
{
|
||||
// Verify admin access
|
||||
$this->authorizeAdminAccess();
|
||||
$this->showBackupSelected = $showBackupSelected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update selected translation IDs from parent component.
|
||||
*/
|
||||
public function updateSelectedTranslationIds(array $ids)
|
||||
{
|
||||
$this->selectedTranslationIds = $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle select/deselect all posts for restore.
|
||||
*/
|
||||
public function toggleSelectAll()
|
||||
{
|
||||
if ($this->selectAllPosts) {
|
||||
$this->selectedPostIndices = array_column($this->restorePostList, 'index');
|
||||
} else {
|
||||
$this->selectedPostIndices = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update selectAllPosts state when individual checkboxes change.
|
||||
*/
|
||||
public function updatedSelectedPostIndices()
|
||||
{
|
||||
$this->selectAllPosts = count($this->selectedPostIndices) === count($this->restorePostList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and download backup for all posts.
|
||||
*/
|
||||
public function backup()
|
||||
{
|
||||
$this->authorizeAdminAccess();
|
||||
|
||||
// Pass query builder (not ->get()) so generateBackup can chunk it
|
||||
$posts = Post::query();
|
||||
|
||||
return $this->generateBackup($posts, 'posts_backup_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and download backup for selected posts only.
|
||||
*/
|
||||
public function backupSelected()
|
||||
{
|
||||
$this->authorizeAdminAccess();
|
||||
|
||||
if (empty($this->selectedTranslationIds)) {
|
||||
$this->notification()->error(
|
||||
__('Error'),
|
||||
__('No posts selected')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get unique post IDs from selected translation IDs
|
||||
$postIds = PostTranslation::whereIn('id', $this->selectedTranslationIds)
|
||||
->pluck('post_id')
|
||||
->unique()
|
||||
->toArray();
|
||||
|
||||
// Pass query builder (not ->get()) so generateBackup can chunk it
|
||||
$posts = Post::whereIn('id', $postIds);
|
||||
|
||||
return $this->generateBackup($posts, 'posts_selected_backup_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate backup data and return as download response (ZIP archive with media).
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Collection $posts Query builder or collection
|
||||
*/
|
||||
private function generateBackup($posts, string $filenamePrefix)
|
||||
{
|
||||
// Build category type lookup (id => type)
|
||||
$categoryTypes = Category::pluck('type', 'id')->toArray();
|
||||
|
||||
$filename = $filenamePrefix . now()->format('Ymd_His') . '.zip';
|
||||
|
||||
// Create temporary files
|
||||
$tempDir = storage_path('app/temp');
|
||||
if (!File::isDirectory($tempDir)) {
|
||||
File::makeDirectory($tempDir, 0755, true);
|
||||
}
|
||||
$tempPath = $tempDir . '/' . uniqid('backup_') . '.zip';
|
||||
$tempJsonPath = $tempDir . '/' . uniqid('backup_json_') . '.json';
|
||||
|
||||
// Track counts for meta
|
||||
$counts = ['posts' => 0, 'post_translations' => 0, 'meetings' => 0, 'media_files' => 0];
|
||||
|
||||
// Track media files to include in ZIP
|
||||
$mediaFiles = [];
|
||||
|
||||
// Write posts as JSON incrementally to a temp file to avoid holding everything in memory
|
||||
$jsonHandle = fopen($tempJsonPath, 'w');
|
||||
// Write a placeholder for meta - will be replaced via a separate meta file in the ZIP
|
||||
fwrite($jsonHandle, '{"meta":"__PLACEHOLDER__","posts":[');
|
||||
|
||||
$isFirst = true;
|
||||
$chunkSize = 100;
|
||||
|
||||
// Process posts in chunks to limit memory usage
|
||||
$processPost = function ($post) use ($categoryTypes, $jsonHandle, &$isFirst, &$counts, &$mediaFiles) {
|
||||
$categoryType = $categoryTypes[$post->category_id] ?? null;
|
||||
|
||||
$postData = [
|
||||
'category_type' => $categoryType,
|
||||
'love_reactant_id' => $post->love_reactant_id,
|
||||
'author_id' => $post->author_id,
|
||||
'author_model' => $post->author_model,
|
||||
'created_at' => $this->formatDate($post->created_at),
|
||||
'updated_at' => $this->formatDate($post->updated_at),
|
||||
'translations' => [],
|
||||
'meeting' => null,
|
||||
'media' => null,
|
||||
];
|
||||
|
||||
foreach ($post->translations as $translation) {
|
||||
$postData['translations'][] = [
|
||||
'locale' => $translation->locale,
|
||||
'slug' => $translation->slug,
|
||||
'title' => $translation->title,
|
||||
'excerpt' => $translation->excerpt,
|
||||
'content' => $translation->content,
|
||||
'status' => $translation->status,
|
||||
'updated_by_user_id' => $translation->updated_by_user_id,
|
||||
'from' => $this->formatDate($translation->from),
|
||||
'till' => $this->formatDate($translation->till),
|
||||
'created_at' => $this->formatDate($translation->created_at),
|
||||
'updated_at' => $this->formatDate($translation->updated_at),
|
||||
];
|
||||
$counts['post_translations']++;
|
||||
}
|
||||
|
||||
if ($post->meeting) {
|
||||
$meeting = $post->meeting;
|
||||
$postData['meeting'] = [
|
||||
'meetingable_type' => $meeting->meetingable_type,
|
||||
'meetingable_name' => $meeting->meetingable?->name,
|
||||
'venue' => $meeting->venue,
|
||||
'address' => $meeting->address,
|
||||
'price' => $meeting->price,
|
||||
'based_on_quantity' => $meeting->based_on_quantity,
|
||||
'transaction_type_id' => $meeting->transaction_type_id,
|
||||
'status' => $meeting->status,
|
||||
'from' => $this->formatDate($meeting->from),
|
||||
'till' => $this->formatDate($meeting->till),
|
||||
'created_at' => $this->formatDate($meeting->created_at),
|
||||
'updated_at' => $this->formatDate($meeting->updated_at),
|
||||
'location' => $this->getLocationNames($meeting->location),
|
||||
];
|
||||
$counts['meetings']++;
|
||||
}
|
||||
|
||||
$media = $post->getFirstMedia('posts');
|
||||
if ($media) {
|
||||
$originalPath = $media->getPath();
|
||||
if (File::exists($originalPath)) {
|
||||
$archivePath = "media/{$post->id}/{$media->file_name}";
|
||||
$postData['media'] = [
|
||||
'name' => $media->name,
|
||||
'file_name' => $media->file_name,
|
||||
'mime_type' => $media->mime_type,
|
||||
'size' => $media->size,
|
||||
'collection_name' => $media->collection_name,
|
||||
'custom_properties' => $media->custom_properties,
|
||||
'archive_path' => $archivePath,
|
||||
];
|
||||
$mediaFiles[] = [
|
||||
'source' => $originalPath,
|
||||
'archive_path' => $archivePath,
|
||||
];
|
||||
$counts['media_files']++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isFirst) {
|
||||
fwrite($jsonHandle, ',');
|
||||
}
|
||||
fwrite($jsonHandle, json_encode($postData, JSON_UNESCAPED_UNICODE));
|
||||
$isFirst = false;
|
||||
$counts['posts']++;
|
||||
};
|
||||
|
||||
// Use chunking for query builders, iterate for collections
|
||||
if ($posts instanceof \Illuminate\Database\Eloquent\Builder) {
|
||||
$posts->with(['translations', 'meeting.location', 'media'])
|
||||
->chunk($chunkSize, function ($chunk) use ($processPost) {
|
||||
foreach ($chunk as $post) {
|
||||
$processPost($post);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
foreach ($posts as $post) {
|
||||
$processPost($post);
|
||||
}
|
||||
}
|
||||
|
||||
// Close JSON array
|
||||
fwrite($jsonHandle, ']}');
|
||||
fclose($jsonHandle);
|
||||
|
||||
// Build meta JSON
|
||||
$meta = json_encode([
|
||||
'version' => '2.0',
|
||||
'created_at' => now()->toIso8601String(),
|
||||
'source_database' => config('database.connections.mysql.database'),
|
||||
'includes_media' => true,
|
||||
'counts' => $counts,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
// Replace the placeholder in the temp JSON file without reading it all into memory
|
||||
// Use a second temp file and stream-copy with the replacement
|
||||
$finalJsonPath = $tempJsonPath . '.final';
|
||||
$inHandle = fopen($tempJsonPath, 'r');
|
||||
$outHandle = fopen($finalJsonPath, 'w');
|
||||
|
||||
// Read the placeholder prefix, replace it, then stream the rest
|
||||
$prefix = fread($inHandle, strlen('{"meta":"__PLACEHOLDER__"'));
|
||||
fwrite($outHandle, '{"meta":' . $meta);
|
||||
|
||||
// Stream the rest of the file in small chunks
|
||||
while (!feof($inHandle)) {
|
||||
fwrite($outHandle, fread($inHandle, 8192));
|
||||
}
|
||||
|
||||
fclose($inHandle);
|
||||
fclose($outHandle);
|
||||
@unlink($tempJsonPath);
|
||||
rename($finalJsonPath, $tempJsonPath);
|
||||
|
||||
// Create ZIP archive
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($tempPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
||||
@unlink($tempJsonPath);
|
||||
$this->notification()->error(
|
||||
__('Error'),
|
||||
__('Failed to create ZIP archive')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$zip->addFile($tempJsonPath, 'backup.json');
|
||||
|
||||
foreach ($mediaFiles as $mediaFile) {
|
||||
if (File::exists($mediaFile['source'])) {
|
||||
$zip->addFile($mediaFile['source'], $mediaFile['archive_path']);
|
||||
}
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
@unlink($tempJsonPath);
|
||||
|
||||
// Move ZIP to storage for download via dedicated route (bypasses Livewire response buffering)
|
||||
$backupsDir = storage_path('app/backups');
|
||||
if (!File::isDirectory($backupsDir)) {
|
||||
File::makeDirectory($backupsDir, 0755, true);
|
||||
}
|
||||
File::move($tempPath, $backupsDir . '/' . $filename);
|
||||
|
||||
$this->notification()->success(
|
||||
__('Backup created'),
|
||||
__('Downloaded :count posts with :media media files', [
|
||||
'count' => $counts['posts'],
|
||||
'media' => $counts['media_files'],
|
||||
])
|
||||
);
|
||||
|
||||
// Dispatch browser event to trigger download via dedicated HTTP route
|
||||
$this->dispatch('backup-ready', filename: $filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the restore modal.
|
||||
*/
|
||||
public function openRestoreModal()
|
||||
{
|
||||
$this->authorizeAdminAccess();
|
||||
$this->cleanupTempFile();
|
||||
$this->reset(['restorePreview', 'restoreStats', 'duplicateAction', 'restorePostList', 'selectedPostIndices', 'selectAllPosts', 'uploadId', 'uploadedFilePath', 'selectedFileName']);
|
||||
$this->showRestoreModal = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive lightweight preview data parsed client-side by JavaScript.
|
||||
* JS extracts meta + post summaries from the backup JSON so we don't
|
||||
* need to send the entire multi-MB JSON string over the wire.
|
||||
*
|
||||
* @param array $meta The backup meta object
|
||||
* @param array $postSummaries Array of {index, title, slug, locales, slugs, has_meeting, has_media, category_type}
|
||||
* @param string $fileName Original file name
|
||||
* @param bool $isZip Whether the file is a ZIP archive
|
||||
*/
|
||||
public function parseBackupPreview(array $meta, array $postSummaries, string $fileName, bool $isZip)
|
||||
{
|
||||
$this->selectedFileName = $fileName;
|
||||
|
||||
try {
|
||||
// Collect all slugs for duplicate checking
|
||||
$allSlugs = [];
|
||||
foreach ($postSummaries as $summary) {
|
||||
foreach ($summary['slugs'] ?? [] as $slug) {
|
||||
$allSlugs[] = $slug;
|
||||
}
|
||||
}
|
||||
|
||||
$existingSlugs = PostTranslation::withTrashed()
|
||||
->whereIn('slug', $allSlugs)
|
||||
->pluck('slug')
|
||||
->toArray();
|
||||
|
||||
$this->restorePreview = [
|
||||
'version' => $meta['version'] ?? 'unknown',
|
||||
'created_at' => $meta['created_at'] ?? 'unknown',
|
||||
'source_database' => $meta['source_database'] ?? 'unknown',
|
||||
'posts' => $meta['counts']['posts'] ?? count($postSummaries),
|
||||
'translations' => $meta['counts']['post_translations'] ?? 0,
|
||||
'meetings' => $meta['counts']['meetings'] ?? 0,
|
||||
'media_files' => $meta['counts']['media_files'] ?? 0,
|
||||
'includes_media' => $meta['includes_media'] ?? false,
|
||||
'duplicates' => count($existingSlugs),
|
||||
'duplicate_slugs' => array_slice($existingSlugs, 0, 10),
|
||||
'is_zip' => $isZip,
|
||||
];
|
||||
|
||||
// Store the post summaries for selection UI (already lightweight)
|
||||
$this->restorePostList = $postSummaries;
|
||||
$this->selectedPostIndices = array_column($postSummaries, 'index');
|
||||
$this->selectAllPosts = true;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->addError('restoreFile', __('Error reading file: ') . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the uploaded file path from a completed chunked upload.
|
||||
* Called by JS after chunk upload finalization succeeds.
|
||||
*/
|
||||
public function setUploadedFilePath(string $uploadId)
|
||||
{
|
||||
$this->authorizeAdminAccess();
|
||||
|
||||
$sessionKey = "backup_restore_file_{$uploadId}";
|
||||
$path = session($sessionKey);
|
||||
|
||||
if (!$path || !File::exists($path)) {
|
||||
$this->notification()->error(__('Error'), __('Uploaded file not found'));
|
||||
return;
|
||||
}
|
||||
|
||||
$this->uploadId = $uploadId;
|
||||
$this->uploadedFilePath = $path;
|
||||
|
||||
// Clean up session key
|
||||
session()->forget($sessionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the restore operation.
|
||||
*/
|
||||
public function restore()
|
||||
{
|
||||
$this->authorizeAdminAccess();
|
||||
|
||||
if (!$this->uploadedFilePath || !File::exists($this->uploadedFilePath)) {
|
||||
$this->notification()->error(__('Error'), __('No file uploaded. Please upload the file first.'));
|
||||
return;
|
||||
}
|
||||
|
||||
$this->isRestoring = true;
|
||||
$extractDir = null;
|
||||
|
||||
try {
|
||||
$filePath = $this->uploadedFilePath;
|
||||
$extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
||||
$isZip = $extension === 'zip';
|
||||
|
||||
$data = null;
|
||||
|
||||
if ($isZip) {
|
||||
// Extract ZIP archive
|
||||
$extractDir = storage_path('app/temp/restore_' . uniqid());
|
||||
File::makeDirectory($extractDir, 0755, true);
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($filePath) !== true) {
|
||||
$this->notification()->error(__('Error'), __('Failed to open ZIP archive'));
|
||||
$this->isRestoring = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate ZIP entries to prevent zip-slip path traversal attacks
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
$entryName = $zip->getNameIndex($i);
|
||||
$fullPath = realpath($extractDir) . '/' . $entryName;
|
||||
if (strpos($fullPath, realpath($extractDir)) !== 0) {
|
||||
$zip->close();
|
||||
File::deleteDirectory($extractDir);
|
||||
$this->notification()->error(__('Error'), __('ZIP archive contains unsafe file paths'));
|
||||
$this->isRestoring = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$zip->extractTo($extractDir);
|
||||
$zip->close();
|
||||
|
||||
$jsonPath = "{$extractDir}/backup.json";
|
||||
if (!File::exists($jsonPath)) {
|
||||
File::deleteDirectory($extractDir);
|
||||
$this->notification()->error(__('Error'), __('Invalid ZIP archive: missing backup.json'));
|
||||
$this->isRestoring = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$data = json_decode(File::get($jsonPath), true);
|
||||
} else {
|
||||
$data = json_decode(file_get_contents($filePath), true);
|
||||
}
|
||||
|
||||
// Get active profile from session
|
||||
$profileId = session('activeProfileId');
|
||||
$profileType = session('activeProfileType');
|
||||
|
||||
if (!$profileId || !$profileType) {
|
||||
if ($extractDir) {
|
||||
File::deleteDirectory($extractDir);
|
||||
}
|
||||
$this->notification()->error(__('Error'), __('No active profile in session'));
|
||||
$this->isRestoring = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Build category type => id lookup
|
||||
$categoryLookup = Category::pluck('id', 'type')->toArray();
|
||||
|
||||
$stats = [
|
||||
'posts_created' => 0,
|
||||
'posts_skipped' => 0,
|
||||
'posts_overwritten' => 0,
|
||||
'translations_created' => 0,
|
||||
'meetings_created' => 0,
|
||||
'media_restored' => 0,
|
||||
'media_skipped' => 0,
|
||||
];
|
||||
|
||||
$createdPostIds = [];
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
// Filter to only selected posts
|
||||
$selectedIndices = array_flip($this->selectedPostIndices);
|
||||
|
||||
// Disable Scout indexing during bulk restore to prevent timeout
|
||||
Post::withoutSyncingToSearch(function () use ($data, $profileId, $profileType, $categoryLookup, &$stats, &$createdPostIds, $extractDir, $isZip, $selectedIndices) {
|
||||
|
||||
foreach ($data['posts'] as $index => $postData) {
|
||||
if (!isset($selectedIndices[$index])) {
|
||||
continue;
|
||||
}
|
||||
// Look up category_id by category_type
|
||||
$categoryId = null;
|
||||
if (!empty($postData['category_type'])) {
|
||||
$categoryId = $categoryLookup[$postData['category_type']] ?? null;
|
||||
}
|
||||
|
||||
// Check for existing slugs
|
||||
if (!empty($postData['translations'])) {
|
||||
$existingSlugs = PostTranslation::withTrashed()
|
||||
->whereIn('slug', array_column($postData['translations'], 'slug'))
|
||||
->pluck('slug')
|
||||
->toArray();
|
||||
|
||||
if (!empty($existingSlugs)) {
|
||||
if ($this->duplicateAction === 'skip') {
|
||||
$stats['posts_skipped']++;
|
||||
continue;
|
||||
} elseif ($this->duplicateAction === 'overwrite') {
|
||||
// Delete existing translations and their posts
|
||||
$existingTranslations = PostTranslation::withTrashed()
|
||||
->whereIn('slug', $existingSlugs)
|
||||
->get();
|
||||
|
||||
foreach ($existingTranslations as $existingTranslation) {
|
||||
$postId = $existingTranslation->post_id;
|
||||
$existingTranslation->forceDelete();
|
||||
|
||||
$remainingTranslations = PostTranslation::withTrashed()
|
||||
->where('post_id', $postId)
|
||||
->count();
|
||||
|
||||
if ($remainingTranslations === 0) {
|
||||
$existingPost = Post::withTrashed()->find($postId);
|
||||
if ($existingPost) {
|
||||
// Clear media before deleting post
|
||||
$existingPost->clearMediaCollection('posts');
|
||||
Meeting::withTrashed()->where('post_id', $postId)->forceDelete();
|
||||
$existingPost->forceDelete();
|
||||
}
|
||||
}
|
||||
}
|
||||
$stats['posts_overwritten']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create post
|
||||
$post = new Post();
|
||||
$post->postable_id = $profileId;
|
||||
$post->postable_type = $profileType;
|
||||
$post->category_id = $categoryId;
|
||||
// Don't set love_reactant_id - let PostObserver register it as reactant
|
||||
$post->author_id = null; // Author IDs are not portable between databases
|
||||
$post->author_model = null;
|
||||
$post->created_at = $postData['created_at'] ? new \DateTime($postData['created_at']) : now();
|
||||
$post->updated_at = $postData['updated_at'] ? new \DateTime($postData['updated_at']) : now();
|
||||
$post->save();
|
||||
|
||||
// Ensure post is registered as reactant (in case observer didn't fire)
|
||||
if (!$post->isRegisteredAsLoveReactant()) {
|
||||
$post->registerAsLoveReactant();
|
||||
}
|
||||
|
||||
// Create translations
|
||||
foreach ($postData['translations'] as $translationData) {
|
||||
$translation = new PostTranslation();
|
||||
$translation->post_id = $post->id;
|
||||
$translation->locale = $translationData['locale'];
|
||||
$translation->slug = $translationData['slug'];
|
||||
$translation->title = $translationData['title'];
|
||||
$translation->excerpt = $translationData['excerpt'];
|
||||
$translation->content = $translationData['content'];
|
||||
$translation->status = $translationData['status'];
|
||||
$translation->updated_by_user_id = $translationData['updated_by_user_id'];
|
||||
$translation->from = $translationData['from'] ? new \DateTime($translationData['from']) : null;
|
||||
$translation->till = $translationData['till'] ? new \DateTime($translationData['till']) : null;
|
||||
$translation->created_at = $translationData['created_at'] ? new \DateTime($translationData['created_at']) : now();
|
||||
$translation->updated_at = $translationData['updated_at'] ? new \DateTime($translationData['updated_at']) : now();
|
||||
$translation->save();
|
||||
|
||||
$stats['translations_created']++;
|
||||
}
|
||||
|
||||
// Create meeting (hasOne relationship)
|
||||
if (!empty($postData['meeting'])) {
|
||||
$meetingData = $postData['meeting'];
|
||||
|
||||
// Look up meetingable by name and type
|
||||
$meetingableId = null;
|
||||
$meetingableType = null;
|
||||
// Whitelist of allowed meetingable types to prevent arbitrary class instantiation
|
||||
$allowedMeetingableTypes = [
|
||||
\App\Models\User::class,
|
||||
\App\Models\Organization::class,
|
||||
\App\Models\Bank::class,
|
||||
];
|
||||
if (!empty($meetingData['meetingable_type']) && !empty($meetingData['meetingable_name'])) {
|
||||
$meetingableType = $meetingData['meetingable_type'];
|
||||
if (in_array($meetingableType, $allowedMeetingableTypes, true)) {
|
||||
$meetingable = $meetingableType::where('name', $meetingData['meetingable_name'])->first();
|
||||
if ($meetingable) {
|
||||
$meetingableId = $meetingable->id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$meeting = new Meeting();
|
||||
$meeting->post_id = $post->id;
|
||||
$meeting->meetingable_id = $meetingableId;
|
||||
$meeting->meetingable_type = $meetingableId ? $meetingableType : null;
|
||||
$meeting->venue = $meetingData['venue'];
|
||||
$meeting->address = $meetingData['address'];
|
||||
$meeting->price = $meetingData['price'];
|
||||
$meeting->based_on_quantity = $meetingData['based_on_quantity'];
|
||||
$meeting->transaction_type_id = $meetingData['transaction_type_id'];
|
||||
$meeting->status = $meetingData['status'];
|
||||
$meeting->from = $meetingData['from'] ? new \DateTime($meetingData['from']) : null;
|
||||
$meeting->till = $meetingData['till'] ? new \DateTime($meetingData['till']) : null;
|
||||
$meeting->created_at = $meetingData['created_at'] ? new \DateTime($meetingData['created_at']) : now();
|
||||
$meeting->updated_at = $meetingData['updated_at'] ? new \DateTime($meetingData['updated_at']) : now();
|
||||
$meeting->save();
|
||||
|
||||
// Create location if location data exists
|
||||
if (!empty($meetingData['location'])) {
|
||||
$locationIds = $this->lookupLocationIds($meetingData['location']);
|
||||
if ($locationIds['country_id'] || $locationIds['division_id'] || $locationIds['city_id'] || $locationIds['district_id']) {
|
||||
$location = new Location();
|
||||
$location->locatable_id = $meeting->id;
|
||||
$location->locatable_type = Meeting::class;
|
||||
$location->country_id = $locationIds['country_id'];
|
||||
$location->division_id = $locationIds['division_id'];
|
||||
$location->city_id = $locationIds['city_id'];
|
||||
$location->district_id = $locationIds['district_id'];
|
||||
$location->save();
|
||||
}
|
||||
}
|
||||
|
||||
$stats['meetings_created']++;
|
||||
}
|
||||
|
||||
// Restore media if available
|
||||
if ($isZip && $extractDir && !empty($postData['media'])) {
|
||||
$mediaData = $postData['media'];
|
||||
$mediaPath = "{$extractDir}/{$mediaData['archive_path']}";
|
||||
|
||||
// Prevent path traversal via crafted archive_path in JSON
|
||||
$realMediaPath = realpath($mediaPath);
|
||||
$realExtractDir = realpath($extractDir);
|
||||
if ($realMediaPath && $realExtractDir && strpos($realMediaPath, $realExtractDir) === 0 && File::exists($mediaPath)) {
|
||||
try {
|
||||
$media = $post->addMedia($mediaPath)
|
||||
->preservingOriginal() // Don't delete from extract dir yet
|
||||
->usingName($mediaData['name'])
|
||||
->usingFileName($mediaData['file_name'])
|
||||
->withCustomProperties($mediaData['custom_properties'] ?? [])
|
||||
->toMediaCollection('posts');
|
||||
|
||||
// Dispatch conversion job to queue
|
||||
$conversionCollection = \Spatie\MediaLibrary\Conversions\ConversionCollection::createForMedia($media);
|
||||
if ($conversionCollection->isNotEmpty()) {
|
||||
dispatch(new \Spatie\MediaLibrary\Conversions\Jobs\PerformConversionsJob($conversionCollection, $media, false))
|
||||
->onQueue('low');
|
||||
}
|
||||
|
||||
$stats['media_restored']++;
|
||||
} catch (\Exception $e) {
|
||||
$stats['media_skipped']++;
|
||||
}
|
||||
} else {
|
||||
$stats['media_skipped']++;
|
||||
}
|
||||
}
|
||||
|
||||
$createdPostIds[] = $post->id;
|
||||
$stats['posts_created']++;
|
||||
}
|
||||
|
||||
}); // End withoutSyncingToSearch
|
||||
|
||||
DB::commit();
|
||||
|
||||
// Clean up extracted files
|
||||
if ($extractDir) {
|
||||
File::deleteDirectory($extractDir);
|
||||
}
|
||||
|
||||
// Queue posts for search indexing on 'low' queue in chunks
|
||||
if (!empty($createdPostIds)) {
|
||||
foreach (array_chunk($createdPostIds, 50) as $chunk) {
|
||||
$posts = Post::whereIn('id', $chunk)->get();
|
||||
dispatch(new MakeSearchable($posts))->onQueue('low');
|
||||
}
|
||||
}
|
||||
|
||||
$this->restoreStats = $stats;
|
||||
|
||||
// Clean up uploaded temp file and free memory
|
||||
if ($this->uploadedFilePath && File::exists($this->uploadedFilePath)) {
|
||||
@unlink($this->uploadedFilePath);
|
||||
}
|
||||
$this->uploadedFilePath = null;
|
||||
$this->uploadId = null;
|
||||
$this->restorePreview = [];
|
||||
$this->restorePostList = [];
|
||||
$this->selectedPostIndices = [];
|
||||
|
||||
$mediaMsg = $stats['media_restored'] > 0 ? " with {$stats['media_restored']} media files" : '';
|
||||
$this->notification()->success(
|
||||
__('Restore completed'),
|
||||
__('Created :count posts', ['count' => $stats['posts_created']]) . $mediaMsg
|
||||
);
|
||||
|
||||
// Refresh parent posts table
|
||||
$this->dispatch('refreshPostsTable')->to('posts.manage');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
if ($extractDir && File::isDirectory($extractDir)) {
|
||||
File::deleteDirectory($extractDir);
|
||||
}
|
||||
$this->notification()->error(__('Error'), $e->getMessage());
|
||||
}
|
||||
|
||||
$this->isRestoring = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close modal and reset state.
|
||||
*/
|
||||
public function closeRestoreModal()
|
||||
{
|
||||
$this->showRestoreModal = false;
|
||||
$this->cleanupTempFile();
|
||||
$this->reset(['restorePreview', 'restoreStats', 'duplicateAction', 'restorePostList', 'selectedPostIndices', 'selectAllPosts', 'uploadId', 'uploadedFilePath', 'selectedFileName']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up any uploaded temp file.
|
||||
*/
|
||||
private function cleanupTempFile(): void
|
||||
{
|
||||
if ($this->uploadedFilePath && File::exists($this->uploadedFilePath)) {
|
||||
@unlink($this->uploadedFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely format a date value to ISO8601 string.
|
||||
*/
|
||||
private function formatDate($value): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value instanceof \Carbon\Carbon || $value instanceof \DateTime) {
|
||||
return $value->format('c');
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get location names in the app's base locale for backup.
|
||||
* Names are used for lookup on restore instead of IDs.
|
||||
*/
|
||||
private function getLocationNames($location): ?array
|
||||
{
|
||||
if (!$location) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$baseLocale = config('app.locale');
|
||||
|
||||
// Get country name
|
||||
$countryName = null;
|
||||
if ($location->country_id) {
|
||||
$countryLocale = CountryLocale::withoutGlobalScopes()
|
||||
->where('country_id', $location->country_id)
|
||||
->where('locale', $baseLocale)
|
||||
->first();
|
||||
$countryName = $countryLocale?->name;
|
||||
}
|
||||
|
||||
// Get division name
|
||||
$divisionName = null;
|
||||
if ($location->division_id) {
|
||||
$divisionLocale = DivisionLocale::withoutGlobalScopes()
|
||||
->where('division_id', $location->division_id)
|
||||
->where('locale', $baseLocale)
|
||||
->first();
|
||||
$divisionName = $divisionLocale?->name;
|
||||
}
|
||||
|
||||
// Get city name
|
||||
$cityName = null;
|
||||
if ($location->city_id) {
|
||||
$cityLocale = CityLocale::withoutGlobalScopes()
|
||||
->where('city_id', $location->city_id)
|
||||
->where('locale', $baseLocale)
|
||||
->first();
|
||||
$cityName = $cityLocale?->name;
|
||||
}
|
||||
|
||||
// Get district name
|
||||
$districtName = null;
|
||||
if ($location->district_id) {
|
||||
$districtLocale = DistrictLocale::withoutGlobalScopes()
|
||||
->where('district_id', $location->district_id)
|
||||
->where('locale', $baseLocale)
|
||||
->first();
|
||||
$districtName = $districtLocale?->name;
|
||||
}
|
||||
|
||||
return [
|
||||
'country_name' => $countryName,
|
||||
'division_name' => $divisionName,
|
||||
'city_name' => $cityName,
|
||||
'district_name' => $districtName,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up location IDs by names in the app's base locale.
|
||||
* Returns null for any location component that cannot be found.
|
||||
*/
|
||||
private function lookupLocationIds(array $locationData): array
|
||||
{
|
||||
$baseLocale = config('app.locale');
|
||||
|
||||
$result = [
|
||||
'country_id' => null,
|
||||
'division_id' => null,
|
||||
'city_id' => null,
|
||||
'district_id' => null,
|
||||
];
|
||||
|
||||
// Look up country by name
|
||||
if (!empty($locationData['country_name'])) {
|
||||
$countryLocale = CountryLocale::withoutGlobalScopes()
|
||||
->where('name', $locationData['country_name'])
|
||||
->where('locale', $baseLocale)
|
||||
->first();
|
||||
$result['country_id'] = $countryLocale?->country_id;
|
||||
}
|
||||
|
||||
// Look up division by name
|
||||
if (!empty($locationData['division_name'])) {
|
||||
$divisionLocale = DivisionLocale::withoutGlobalScopes()
|
||||
->where('name', $locationData['division_name'])
|
||||
->where('locale', $baseLocale)
|
||||
->first();
|
||||
$result['division_id'] = $divisionLocale?->division_id;
|
||||
}
|
||||
|
||||
// Look up city by name
|
||||
if (!empty($locationData['city_name'])) {
|
||||
$cityLocale = CityLocale::withoutGlobalScopes()
|
||||
->where('name', $locationData['city_name'])
|
||||
->where('locale', $baseLocale)
|
||||
->first();
|
||||
$result['city_id'] = $cityLocale?->city_id;
|
||||
}
|
||||
|
||||
// Look up district by name
|
||||
if (!empty($locationData['district_name'])) {
|
||||
$districtLocale = DistrictLocale::withoutGlobalScopes()
|
||||
->where('name', $locationData['district_name'])
|
||||
->where('locale', $baseLocale)
|
||||
->first();
|
||||
$result['district_id'] = $districtLocale?->district_id;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.posts.backup-restore');
|
||||
}
|
||||
}
|
||||
13
app/Http/Livewire/Posts/Create.php
Normal file
13
app/Http/Livewire/Posts/Create.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Posts;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class Create extends Component
|
||||
{
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.posts.create');
|
||||
}
|
||||
}
|
||||
1684
app/Http/Livewire/Posts/Manage.php
Normal file
1684
app/Http/Livewire/Posts/Manage.php
Normal file
File diff suppressed because it is too large
Load Diff
16
app/Http/Livewire/Posts/ManageActions.php
Normal file
16
app/Http/Livewire/Posts/ManageActions.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Posts;
|
||||
|
||||
use App\Models\Post;
|
||||
use Livewire\Component;
|
||||
|
||||
class ManageActions extends Component
|
||||
{
|
||||
public Post $post;
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.posts.manage-actions');
|
||||
}
|
||||
}
|
||||
196
app/Http/Livewire/Posts/SelectAuthor.php
Normal file
196
app/Http/Livewire/Posts/SelectAuthor.php
Normal file
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Posts;
|
||||
|
||||
use App\Models\Bank;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Component;
|
||||
|
||||
class SelectAuthor extends Component
|
||||
{
|
||||
public $search;
|
||||
public $searchResults = [];
|
||||
public $showDropdown = false;
|
||||
public $selectedId;
|
||||
public $selected = [];
|
||||
|
||||
// Properties for receiving author data from parent
|
||||
public $authorId;
|
||||
public $authorModel;
|
||||
|
||||
protected $listeners = [
|
||||
'resetForm',
|
||||
'authorExists'
|
||||
];
|
||||
|
||||
public function mount($authorId = null, $authorModel = null)
|
||||
{
|
||||
$this->authorId = $authorId;
|
||||
$this->authorModel = $authorModel;
|
||||
|
||||
// If author data is provided, populate the selected data
|
||||
if ($this->authorId && $this->authorModel) {
|
||||
$this->populateAuthorData($this->authorId, $this->authorModel);
|
||||
}
|
||||
}
|
||||
|
||||
private function populateAuthorData($authorId, $authorModel)
|
||||
{
|
||||
$this->selectedId = $authorId;
|
||||
$this->selected['id'] = $authorId;
|
||||
$this->selected['type'] = $authorModel;
|
||||
|
||||
try {
|
||||
if ($authorModel == User::class) {
|
||||
$author = User::where('id', $authorId)->select('name', 'profile_photo_path')->firstOrFail();
|
||||
$description = '';
|
||||
} elseif ($authorModel == Organization::class) {
|
||||
$author = Organization::where('id', $authorId)->select('name', 'profile_photo_path')->firstOrFail();
|
||||
$description = __('Organization');
|
||||
} elseif ($authorModel == Bank::class) {
|
||||
$author = Bank::where('id', $authorId)->select('name', 'profile_photo_path')->firstOrFail();
|
||||
$description = __('Bank');
|
||||
} else {
|
||||
// Unknown author model type
|
||||
return;
|
||||
}
|
||||
|
||||
$this->selected['name'] = $author->name;
|
||||
$this->selected['profile_photo_path'] = url(Storage::url($author->profile_photo_path));
|
||||
$this->selected['description'] = $description;
|
||||
} catch (\Exception) {
|
||||
// Silently fail if author data cannot be loaded
|
||||
$this->selectedId = null;
|
||||
$this->selected = [];
|
||||
}
|
||||
}
|
||||
|
||||
public function inputBlur()
|
||||
{
|
||||
$this->dispatch('toAuthorValidation');
|
||||
$this->showDropdown = false;
|
||||
$this->search = '';
|
||||
}
|
||||
|
||||
public function resetForm()
|
||||
{
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate dropdown when already author data exists
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
public function authorExists($value)
|
||||
{
|
||||
$this->selectedId = $value['author_id'];
|
||||
$this->selected['id'] = $value['author_id'];
|
||||
$this->selected['type'] = $value['author_model'];
|
||||
|
||||
if ($value['author_model'] == User::class) {
|
||||
$author = User::where('id', $value['author_id'])->select('name', 'profile_photo_path')->firstOrFail();
|
||||
$description = '';
|
||||
} elseif ($value['author_model'] == Organization::class) {
|
||||
$author = Organization::where('id', $value['author_id'])->select('name', 'profile_photo_path')->firstOrFail();
|
||||
$description = __('Organization');
|
||||
} elseif ($value['author_model'] == Bank::class) {
|
||||
$author = Bank::where('id', $value['author_id'])->select('name', 'profile_photo_path')->firstOrFail();
|
||||
$description = __('Bank');
|
||||
}
|
||||
|
||||
$this->selected['name'] = $author->name;
|
||||
$this->selected['profile_photo_path'] = url(Storage::url($author->profile_photo_path));
|
||||
$this->selected['description'] = $description;
|
||||
}
|
||||
|
||||
|
||||
public function authorSelected($value)
|
||||
{
|
||||
$this->selectedId = $value;
|
||||
$this->selected = collect($this->searchResults)->where('id', '=', $value)->first();
|
||||
$this->showDropdown = false;
|
||||
$this->search = '';
|
||||
$this->dispatch('authorSelected', $this->selected);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* updatedSearch: Search query users, organizations, and banks
|
||||
*
|
||||
* @param mixed $newValue
|
||||
* @return void
|
||||
*/
|
||||
public function updatedSearch()
|
||||
{
|
||||
$this->showDropdown = true;
|
||||
$search = $this->search;
|
||||
|
||||
// Search Users
|
||||
$users = User::where('name', 'like', '%' . $search . '%')
|
||||
->where('id', '!=', '1') // Exclude Super-Admin user //TODO: exclude all admin users by role
|
||||
->select('id', 'name', 'profile_photo_path')
|
||||
->get();
|
||||
$users = $users->map(function ($item) {
|
||||
return
|
||||
[
|
||||
'id' => $item['id'],
|
||||
'type' => User::class,
|
||||
'name' => $item['name'],
|
||||
'description' => '',
|
||||
'profile_photo_path' => url('/storage/' . $item['profile_photo_path'])
|
||||
];
|
||||
});
|
||||
|
||||
// Search Organizations
|
||||
$organizations = Organization::where('name', 'like', '%' . $search . '%')
|
||||
->select('id', 'name', 'profile_photo_path')
|
||||
->get();
|
||||
$organizations = $organizations->map(function ($item) {
|
||||
return
|
||||
[
|
||||
'id' => $item['id'],
|
||||
'type' => Organization::class,
|
||||
'name' => $item['name'],
|
||||
'description' => __('Organization'),
|
||||
'profile_photo_path' => url(Storage::url($item['profile_photo_path']))
|
||||
];
|
||||
});
|
||||
|
||||
// Search Banks
|
||||
$banks = Bank::where('name', 'like', '%' . $search . '%')
|
||||
->select('id', 'name', 'profile_photo_path')
|
||||
->get();
|
||||
$banks = $banks->map(function ($item) {
|
||||
return
|
||||
[
|
||||
'id' => $item['id'],
|
||||
'type' => Bank::class,
|
||||
'name' => $item['name'],
|
||||
'description' => __('Bank'),
|
||||
'profile_photo_path' => url(Storage::url($item['profile_photo_path']))
|
||||
];
|
||||
});
|
||||
|
||||
// Merge all results
|
||||
$merged = collect($users)->merge($organizations)->merge($banks);
|
||||
$response = $merged->take(6);
|
||||
$this->searchResults = $response;
|
||||
}
|
||||
|
||||
public function removeSelectedProfile()
|
||||
{
|
||||
$this->selectedId = null;
|
||||
$this->selected = [];
|
||||
$this->dispatch('authorSelected', ['id' => null, 'type' => null]);
|
||||
}
|
||||
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.posts.select-author');
|
||||
}
|
||||
}
|
||||
138
app/Http/Livewire/Posts/SelectOrganizer.php
Normal file
138
app/Http/Livewire/Posts/SelectOrganizer.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Posts;
|
||||
|
||||
use App\Models\Bank;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Component;
|
||||
|
||||
class SelectOrganizer extends Component
|
||||
{
|
||||
public $search;
|
||||
public $searchResults = [];
|
||||
public $showDropdown = false;
|
||||
public $selectedId;
|
||||
public $selected = [];
|
||||
|
||||
protected $listeners = [
|
||||
'resetForm',
|
||||
'organizerExists'
|
||||
];
|
||||
|
||||
|
||||
public function inputBlur()
|
||||
{
|
||||
$this->dispatch('toAccountValidation');
|
||||
$this->showDropdown = false;
|
||||
$this->search = '';
|
||||
}
|
||||
|
||||
public function resetForm()
|
||||
{
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate dropdown when already meeting data exists
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
public function organizerExists($value)
|
||||
{
|
||||
$this->selectedId = $value['meetingable_id'];
|
||||
$this->selected['id'] = $value['meetingable_id'];
|
||||
$this->selected['type'] = $value['meetingable_type'];
|
||||
if ($value['meetingable_type'] == User::class) {
|
||||
$organizer = User::where('id', $value['meetingable_id'])->select('name', 'profile_photo_path')->firstOrFail();
|
||||
$description = '';
|
||||
} elseif ($value['meetingable_type'] == Bank::class) {
|
||||
$organizer = Bank::where('id', $value['meetingable_id'])->select('name', 'profile_photo_path')->firstOrFail();
|
||||
$description = __('Bank');
|
||||
} else {
|
||||
$organizer = Organization::where('id', $value['meetingable_id'])->select('name', 'profile_photo_path')->firstOrFail();
|
||||
$description = __('Organization');
|
||||
}
|
||||
$this->selected['name'] = $organizer->name;
|
||||
$this->selected['profile_photo_path'] = url(Storage::url($organizer->profile_photo_path));
|
||||
$this->selected['description'] = $description;
|
||||
}
|
||||
|
||||
|
||||
public function orgSelected($value)
|
||||
{
|
||||
$this->selectedId = $value;
|
||||
$this->selected = collect($this->searchResults)->where('id', '=', $value)->first();
|
||||
$this->showDropdown = false;
|
||||
$this->search = '';
|
||||
$this->dispatch('organizerSelected', $this->selected);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* updatedSearch: Search query users and organizations
|
||||
*
|
||||
* @param mixed $newValue
|
||||
* @return void
|
||||
*/
|
||||
public function updatedSearch($newValue)
|
||||
{
|
||||
$this->showDropdown = true;
|
||||
$search = $this->search;
|
||||
$users = User::where('name', 'like', '%' . $search . '%')
|
||||
->where('id', '!=', '1') // Exclude Super-Admin user //TODO: exclude all admin users by role
|
||||
->select('id', 'name', 'profile_photo_path')
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
return [
|
||||
'id' => $item['id'],
|
||||
'type' => User::class,
|
||||
'name' => $item['name'],
|
||||
'description' => '',
|
||||
'profile_photo_path' => url('/storage/' . $item['profile_photo_path'])
|
||||
];
|
||||
});
|
||||
$organizations = Organization::where('name', 'like', '%' . $search . '%')
|
||||
->select('id', 'name', 'profile_photo_path')
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
return [
|
||||
'id' => $item['id'],
|
||||
'type' => Organization::class,
|
||||
'name' => $item['name'],
|
||||
'description' => __('Organization'),
|
||||
'profile_photo_path' => url(Storage::url($item['profile_photo_path']))
|
||||
];
|
||||
});
|
||||
$banks = Bank::where('name', 'like', '%' . $search . '%')
|
||||
->select('id', 'name', 'profile_photo_path')
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
return [
|
||||
'id' => $item['id'],
|
||||
'type' => Bank::class,
|
||||
'name' => $item['name'],
|
||||
'description' => __('Bank'),
|
||||
'profile_photo_path' => url(Storage::url($item['profile_photo_path']))
|
||||
];
|
||||
});
|
||||
$merged = $users->concat($organizations)->concat($banks);
|
||||
$response = $merged->take(6);
|
||||
$this->searchResults = $response->toArray();
|
||||
}
|
||||
|
||||
public function removeSelectedProfile()
|
||||
{
|
||||
$this->selectedId = null;
|
||||
$this->selected = [];
|
||||
$this->dispatch('organizerSelected', $this->selected);
|
||||
}
|
||||
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.posts.select-organizer');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user