0, 'failed' => 0, 'errors' => []]; // Validate ownership — fetch only artworks belonging to this user $query = Artwork::where('user_id', $userId); if ($action === 'unarchive') { $query->onlyTrashed(); } $artworks = $query->whereIn('id', $artworkIds)->get(); $foundIds = $artworks->pluck('id')->all(); $missingIds = array_diff($artworkIds, $foundIds); foreach ($missingIds as $id) { $result['failed']++; $result['errors'][] = "Artwork #{$id}: not found or not owned by you"; } if ($artworks->isEmpty()) { return $result; } DB::beginTransaction(); try { foreach ($artworks as $artwork) { $this->applyAction($artwork, $action, $params); $result['success']++; } DB::commit(); // Reindex affected artworks in Meilisearch $this->reindexArtworks($artworks); Log::info('Studio bulk action completed', [ 'user_id' => $userId, 'action' => $action, 'count' => $result['success'], 'ids' => $foundIds, ]); } catch (\Throwable $e) { DB::rollBack(); $result['failed'] += $result['success']; $result['success'] = 0; $result['errors'][] = 'Transaction failed: ' . $e->getMessage(); Log::error('Studio bulk action failed', [ 'user_id' => $userId, 'action' => $action, 'error' => $e->getMessage(), ]); } return $result; } private function applyAction(Artwork $artwork, string $action, array $params): void { match ($action) { 'publish' => $this->publish($artwork), 'unpublish' => $this->unpublish($artwork), 'archive' => $artwork->delete(), // Soft delete 'unarchive' => $artwork->restore(), 'delete' => $artwork->forceDelete(), 'change_category' => $this->changeCategory($artwork, $params), 'add_tags' => $this->addTags($artwork, $params), 'remove_tags' => $this->removeTags($artwork, $params), default => throw new \InvalidArgumentException("Unknown action: {$action}"), }; } private function publish(Artwork $artwork): void { $artwork->update([ 'is_public' => true, 'published_at' => $artwork->published_at ?? now(), ]); } private function unpublish(Artwork $artwork): void { $artwork->update(['is_public' => false]); } private function changeCategory(Artwork $artwork, array $params): void { if (empty($params['category_id'])) { throw new \InvalidArgumentException('category_id required for change_category'); } $artwork->categories()->sync([(int) $params['category_id']]); } private function addTags(Artwork $artwork, array $params): void { if (empty($params['tag_ids'])) { throw new \InvalidArgumentException('tag_ids required for add_tags'); } $pivotData = []; foreach ((array) $params['tag_ids'] as $tagId) { $pivotData[(int) $tagId] = ['source' => 'studio_bulk', 'confidence' => 1.0]; } $artwork->tags()->syncWithoutDetaching($pivotData); // Increment usage counts Tag::whereIn('id', array_keys($pivotData)) ->increment('usage_count'); } private function removeTags(Artwork $artwork, array $params): void { if (empty($params['tag_ids'])) { throw new \InvalidArgumentException('tag_ids required for remove_tags'); } $tagIds = array_map('intval', (array) $params['tag_ids']); $artwork->tags()->detach($tagIds); Tag::whereIn('id', $tagIds) ->where('usage_count', '>', 0) ->decrement('usage_count'); } /** * Trigger Meilisearch reindex for the given artworks. */ private function reindexArtworks(\Illuminate\Database\Eloquent\Collection $artworks): void { try { $artworks->each->searchable(); } catch (\Throwable $e) { Log::warning('Studio: Failed to reindex artworks after bulk action', [ 'error' => $e->getMessage(), ]); } } }