|
@@ -4,13 +4,9 @@ namespace Longyi\Core\Http\Controllers\Admin;
|
|
|
|
|
|
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\JsonResponse;
|
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Request;
|
|
|
-use Illuminate\Http\UploadedFile;
|
|
|
|
|
use Illuminate\Routing\Controller;
|
|
use Illuminate\Routing\Controller;
|
|
|
use Illuminate\Foundation\Validation\ValidatesRequests;
|
|
use Illuminate\Foundation\Validation\ValidatesRequests;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
-use Illuminate\Support\Facades\Storage;
|
|
|
|
|
-use Illuminate\Support\Str;
|
|
|
|
|
-use Intervention\Image\ImageManager;
|
|
|
|
|
use Longyi\Core\Helpers\FlexibleVariantOption;
|
|
use Longyi\Core\Helpers\FlexibleVariantOption;
|
|
|
use Longyi\Core\Models\ProductOption;
|
|
use Longyi\Core\Models\ProductOption;
|
|
|
use Longyi\Core\Models\ProductOptionValue;
|
|
use Longyi\Core\Models\ProductOptionValue;
|
|
@@ -18,6 +14,7 @@ use Longyi\Core\Models\ProductVariant;
|
|
|
use Longyi\Core\Repositories\ProductOptionRepository;
|
|
use Longyi\Core\Repositories\ProductOptionRepository;
|
|
|
use Longyi\Core\Repositories\ProductOptionValueRepository;
|
|
use Longyi\Core\Repositories\ProductOptionValueRepository;
|
|
|
use Longyi\Core\Repositories\ProductVariantRepository;
|
|
use Longyi\Core\Repositories\ProductVariantRepository;
|
|
|
|
|
+use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
|
|
use Webkul\Product\Helpers\Indexers\Price as PriceIndexer;
|
|
use Webkul\Product\Helpers\Indexers\Price as PriceIndexer;
|
|
|
use Webkul\Product\Repositories\ProductRepository;
|
|
use Webkul\Product\Repositories\ProductRepository;
|
|
|
|
|
|
|
@@ -351,7 +348,7 @@ class FlexibleVariantController extends Controller
|
|
|
'variants.*.values' => 'array',
|
|
'variants.*.values' => 'array',
|
|
|
'variants.*.values.*.code' => 'required|string',
|
|
'variants.*.values.*.code' => 'required|string',
|
|
|
'variants.*.image_ids' => 'array',
|
|
'variants.*.image_ids' => 'array',
|
|
|
- 'variants.*.image_ids.*' => 'integer|exists:product_images,id',
|
|
|
|
|
|
|
+ 'variants.*.image_ids.*' => 'integer|exists:media,id',
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
@@ -420,8 +417,8 @@ class FlexibleVariantController extends Controller
|
|
|
if (isset($variantData['image_ids'])) {
|
|
if (isset($variantData['image_ids'])) {
|
|
|
$imageSync = collect($variantData['image_ids'])
|
|
$imageSync = collect($variantData['image_ids'])
|
|
|
->values()
|
|
->values()
|
|
|
- ->mapWithKeys(fn ($imageId, $i) => [$imageId => ['position' => $i]]);
|
|
|
|
|
- $variant->images()->sync($imageSync);
|
|
|
|
|
|
|
+ ->mapWithKeys(fn ($mediaId, $i) => [$mediaId => ['position' => $i]]);
|
|
|
|
|
+ $variant->variantImages()->sync($imageSync);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
$variants[$variantIndex]['variant_id'] = $variant->id;
|
|
$variants[$variantIndex]['variant_id'] = $variant->id;
|
|
@@ -660,71 +657,129 @@ class FlexibleVariantController extends Controller
|
|
|
/**
|
|
/**
|
|
|
* Sync images for a variant.
|
|
* Sync images for a variant.
|
|
|
*
|
|
*
|
|
|
- * Accepts a mix of existing product_image IDs and new file uploads.
|
|
|
|
|
- * New files are stored into the product gallery first, then all IDs
|
|
|
|
|
- * are synced to the variant via the pivot table.
|
|
|
|
|
- *
|
|
|
|
|
* POST /variants/{id}/images
|
|
* POST /variants/{id}/images
|
|
|
* Body (multipart/form-data):
|
|
* Body (multipart/form-data):
|
|
|
- * image_ids[] - existing product_image IDs to keep/attach
|
|
|
|
|
- * uploads[] - new image files to upload
|
|
|
|
|
|
|
+ * media_ids[] - existing Media IDs (spatie/medialibrary) to attach/keep
|
|
|
|
|
+ * uploads[] - new image files to upload (stored via medialibrary on the variant)
|
|
|
|
|
+ *
|
|
|
|
|
+ * Workflow:
|
|
|
|
|
+ * 1. New uploads are added to the variant's 'variant_images' media collection.
|
|
|
|
|
+ * 2. All resulting Media IDs (new + existing) are synced into
|
|
|
|
|
+ * the product_variant_images pivot table with position order.
|
|
|
*/
|
|
*/
|
|
|
public function syncVariantImages(int $id, Request $request): JsonResponse
|
|
public function syncVariantImages(int $id, Request $request): JsonResponse
|
|
|
{
|
|
{
|
|
|
$this->validate($request, [
|
|
$this->validate($request, [
|
|
|
- 'image_ids' => 'array',
|
|
|
|
|
- 'image_ids.*' => 'integer|exists:product_images,id',
|
|
|
|
|
|
|
+ 'media_ids' => 'array',
|
|
|
|
|
+ 'media_ids.*' => 'integer|exists:media,id',
|
|
|
'uploads' => 'array',
|
|
'uploads' => 'array',
|
|
|
'uploads.*' => 'image|max:5120',
|
|
'uploads.*' => 'image|max:5120',
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
$variant = ProductVariant::findOrFail($id);
|
|
$variant = ProductVariant::findOrFail($id);
|
|
|
- $product = $variant->product;
|
|
|
|
|
|
|
|
|
|
- $imageIds = collect($request->input('image_ids', []));
|
|
|
|
|
|
|
+ $mediaIds = collect($request->input('media_ids', []));
|
|
|
|
|
|
|
|
if ($request->hasFile('uploads')) {
|
|
if ($request->hasFile('uploads')) {
|
|
|
foreach ($request->file('uploads') as $file) {
|
|
foreach ($request->file('uploads') as $file) {
|
|
|
- $newImage = $this->storeProductImage($file, $product);
|
|
|
|
|
- $imageIds->push($newImage->id);
|
|
|
|
|
|
|
+ $media = $variant
|
|
|
|
|
+ ->addMedia($file)
|
|
|
|
|
+ ->toMediaCollection(ProductVariant::IMAGES_COLLECTION);
|
|
|
|
|
+
|
|
|
|
|
+ $mediaIds->push($media->id);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- $syncData = $imageIds->values()->mapWithKeys(
|
|
|
|
|
- fn ($imageId, $i) => [$imageId => ['position' => $i]]
|
|
|
|
|
|
|
+ $syncData = $mediaIds->values()->mapWithKeys(
|
|
|
|
|
+ fn ($mediaId, $i) => [$mediaId => ['position' => $i]]
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
- $variant->images()->sync($syncData);
|
|
|
|
|
|
|
+ $variant->variantImages()->sync($syncData);
|
|
|
|
|
|
|
|
return response()->json([
|
|
return response()->json([
|
|
|
'success' => true,
|
|
'success' => true,
|
|
|
- 'data' => $variant->load('images')->images,
|
|
|
|
|
|
|
+ 'data' => $this->formatVariantImages($variant),
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Upload a new image for a variant without replacing existing ones.
|
|
|
|
|
+ *
|
|
|
|
|
+ * POST /variants/{id}/images/upload
|
|
|
|
|
+ */
|
|
|
|
|
+ public function uploadVariantImage(int $id, Request $request): JsonResponse
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->validate($request, [
|
|
|
|
|
+ 'file' => 'required|image|max:5120',
|
|
|
]);
|
|
]);
|
|
|
|
|
+
|
|
|
|
|
+ $variant = ProductVariant::findOrFail($id);
|
|
|
|
|
+
|
|
|
|
|
+ $media = $variant
|
|
|
|
|
+ ->addMedia($request->file('file'))
|
|
|
|
|
+ ->toMediaCollection(ProductVariant::IMAGES_COLLECTION);
|
|
|
|
|
+
|
|
|
|
|
+ // Append to pivot table at the end
|
|
|
|
|
+ $nextPosition = $variant->variantImages()->count();
|
|
|
|
|
+ $variant->variantImages()->attach($media->id, ['position' => $nextPosition]);
|
|
|
|
|
+
|
|
|
|
|
+ return response()->json([
|
|
|
|
|
+ 'success' => true,
|
|
|
|
|
+ 'data' => $this->formatMediaItem($media),
|
|
|
|
|
+ ], 201);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Store an uploaded file into the product gallery (product_images table).
|
|
|
|
|
|
|
+ * Remove a single media item from a variant (detach from pivot + delete media record).
|
|
|
|
|
+ *
|
|
|
|
|
+ * DELETE /variants/{id}/images/{mediaId}
|
|
|
*/
|
|
*/
|
|
|
- protected function storeProductImage(UploadedFile $file, $product): \Webkul\Product\Models\ProductImage
|
|
|
|
|
|
|
+ public function removeVariantImage(int $id, int $mediaId): JsonResponse
|
|
|
{
|
|
{
|
|
|
- $directory = 'product/' . $product->id;
|
|
|
|
|
-
|
|
|
|
|
- if (Str::contains($file->getMimeType(), 'image')) {
|
|
|
|
|
- $manager = new ImageManager;
|
|
|
|
|
- $image = $manager->make($file)->encode('webp');
|
|
|
|
|
- $path = $directory . '/' . Str::random(40) . '.webp';
|
|
|
|
|
- Storage::put($path, $image);
|
|
|
|
|
- } else {
|
|
|
|
|
- $path = $file->store($directory);
|
|
|
|
|
|
|
+ $variant = ProductVariant::findOrFail($id);
|
|
|
|
|
+
|
|
|
|
|
+ $variant->variantImages()->detach($mediaId);
|
|
|
|
|
+
|
|
|
|
|
+ $media = Media::find($mediaId);
|
|
|
|
|
+
|
|
|
|
|
+ // Only delete the media record if no other variant still references it
|
|
|
|
|
+ if ($media && $variant->variantImages()->where('media_id', $mediaId)->doesntExist()) {
|
|
|
|
|
+ $media->delete();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return $product->images()->create([
|
|
|
|
|
- 'type' => 'images',
|
|
|
|
|
- 'path' => $path,
|
|
|
|
|
- 'position' => $product->images()->count(),
|
|
|
|
|
|
|
+ return response()->json([
|
|
|
|
|
+ 'success' => true,
|
|
|
|
|
+ 'message' => 'Image removed successfully',
|
|
|
]);
|
|
]);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Format a variant's images for API response.
|
|
|
|
|
+ */
|
|
|
|
|
+ protected function formatVariantImages(ProductVariant $variant): array
|
|
|
|
|
+ {
|
|
|
|
|
+ return $variant->variantImages()
|
|
|
|
|
+ ->get()
|
|
|
|
|
+ ->map(fn ($media) => $this->formatMediaItem($media))
|
|
|
|
|
+ ->all();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Format a single Media item for API response.
|
|
|
|
|
+ */
|
|
|
|
|
+ protected function formatMediaItem(Media $media): array
|
|
|
|
|
+ {
|
|
|
|
|
+ return [
|
|
|
|
|
+ 'id' => $media->id,
|
|
|
|
|
+ 'url' => $media->getUrl(),
|
|
|
|
|
+ 'thumb_url' => $media->hasGeneratedConversion('thumb') ? $media->getUrl('thumb') : $media->getUrl(),
|
|
|
|
|
+ 'file_name' => $media->file_name,
|
|
|
|
|
+ 'mime_type' => $media->mime_type,
|
|
|
|
|
+ 'size' => $media->size,
|
|
|
|
|
+ 'position' => $media->pivot?->position,
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Trigger price reindex for a product and its flexible variants.
|
|
* Trigger price reindex for a product and its flexible variants.
|
|
|
*/
|
|
*/
|