- Replace sidebar layout with top header design - Add product image thumbnail, badges (Active/Featured), and action buttons - Implement real-time badge toggling with inline JavaScript - Add one-active-product-per-brand validation with force-activate option - Standardize checkbox styling with DaisyUI components - Update terminology from "Default" to "Primary" for images - Add new models: ProductLine, ProductPackaging, Unit - Add product line management and image sorting - Add styling rules to CLAUDE.md for consistency 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
167 lines
5.2 KiB
PHP
167 lines
5.2 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Seller;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Business;
|
|
use App\Models\Product;
|
|
use App\Models\ProductImage;
|
|
use App\Traits\FileStorageHelper;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
class ProductImageController extends Controller
|
|
{
|
|
use FileStorageHelper;
|
|
|
|
/**
|
|
* Upload a new product image
|
|
*/
|
|
public function upload(Request $request, Business $business, Product $product)
|
|
{
|
|
// CRITICAL: Ensure product belongs to this business through brand
|
|
$product = Product::whereHas('brand', function ($query) use ($business) {
|
|
$query->where('business_id', $business->id);
|
|
})->findOrFail($product->id);
|
|
|
|
// Validate image
|
|
$request->validate([
|
|
'image' => 'required|image|mimes:jpeg,jpg,png|max:2048|dimensions:min_width=750,min_height=384', // 2MB max, 750x384 min
|
|
]);
|
|
|
|
// Check if product already has 6 images
|
|
if ($product->images()->count() >= 6) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Maximum of 6 images allowed per product'
|
|
], 422);
|
|
}
|
|
|
|
// Store the image using trait method
|
|
$path = $this->storeFile($request->file('image'), 'products');
|
|
|
|
// Determine if this should be the primary image (first one)
|
|
$isPrimary = $product->images()->count() === 0;
|
|
|
|
// If setting as primary, unset other primary images
|
|
if ($isPrimary) {
|
|
$product->images()->update(['is_primary' => false]);
|
|
}
|
|
|
|
// Create the image record
|
|
$image = $product->images()->create([
|
|
'path' => $path,
|
|
'is_primary' => $isPrimary,
|
|
'sort_order' => $product->images()->max('sort_order') + 1,
|
|
]);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'image' => [
|
|
'id' => $image->id,
|
|
'path' => $image->path,
|
|
'is_primary' => $image->is_primary,
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Delete a product image
|
|
*/
|
|
public function delete(Business $business, Product $product, ProductImage $image)
|
|
{
|
|
// CRITICAL: Ensure product belongs to this business through brand
|
|
$product = Product::whereHas('brand', function ($query) use ($business) {
|
|
$query->where('business_id', $business->id);
|
|
})->findOrFail($product->id);
|
|
|
|
// Ensure image belongs to this product
|
|
if ($image->product_id !== $product->id) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Image not found'
|
|
], 404);
|
|
}
|
|
|
|
// Delete the file from storage using trait method
|
|
$this->deleteFile($image->path);
|
|
|
|
// If deleting primary image, set next image as primary
|
|
if ($image->is_primary) {
|
|
$nextImage = $product->images()
|
|
->where('id', '!=', $image->id)
|
|
->orderBy('sort_order')
|
|
->first();
|
|
|
|
if ($nextImage) {
|
|
$nextImage->update(['is_primary' => true]);
|
|
}
|
|
}
|
|
|
|
$image->delete();
|
|
|
|
return response()->json(['success' => true]);
|
|
}
|
|
|
|
/**
|
|
* Reorder product images
|
|
*/
|
|
public function reorder(Request $request, Business $business, Product $product)
|
|
{
|
|
// CRITICAL: Ensure product belongs to this business through brand
|
|
$product = Product::whereHas('brand', function ($query) use ($business) {
|
|
$query->where('business_id', $business->id);
|
|
})->findOrFail($product->id);
|
|
|
|
$request->validate([
|
|
'order' => 'required|array',
|
|
'order.*' => 'required|integer|exists:product_images,id'
|
|
]);
|
|
|
|
$order = $request->input('order');
|
|
|
|
// Update sort order and set first image as primary
|
|
foreach ($order as $index => $imageId) {
|
|
$image = ProductImage::where('id', $imageId)
|
|
->where('product_id', $product->id)
|
|
->first();
|
|
|
|
if ($image) {
|
|
$image->update([
|
|
'sort_order' => $index,
|
|
'is_primary' => $index === 0, // First image is primary
|
|
]);
|
|
}
|
|
}
|
|
|
|
return response()->json(['success' => true]);
|
|
}
|
|
|
|
/**
|
|
* Set an image as primary
|
|
*/
|
|
public function setPrimary(Business $business, Product $product, ProductImage $image)
|
|
{
|
|
// CRITICAL: Ensure product belongs to this business through brand
|
|
$product = Product::whereHas('brand', function ($query) use ($business) {
|
|
$query->where('business_id', $business->id);
|
|
})->findOrFail($product->id);
|
|
|
|
// Ensure image belongs to this product
|
|
if ($image->product_id !== $product->id) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Image not found'
|
|
], 404);
|
|
}
|
|
|
|
// Unset all primary flags for this product
|
|
$product->images()->update(['is_primary' => false]);
|
|
|
|
// Set this image as primary
|
|
$image->update(['is_primary' => true]);
|
|
|
|
return response()->json(['success' => true]);
|
|
}
|
|
}
|