288 lines
9.2 KiB
PHP
288 lines
9.2 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\Brand;
|
|
use App\Models\Product;
|
|
use App\Models\ProductCategory;
|
|
use App\Models\Strain;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class MarketplaceController extends Controller
|
|
{
|
|
/**
|
|
* Display marketplace browse page (Amazon/Shopify style)
|
|
*/
|
|
public function index(Request $request)
|
|
{
|
|
$business = auth()->user()->businesses->first();
|
|
$hasFilters = $request->hasAny(['search', 'brand_id', 'strain_type', 'price_min', 'price_max', 'in_stock', 'category_id']);
|
|
|
|
// Start with active products only
|
|
$query = Product::query()
|
|
->with(['brand', 'strain', 'category'])
|
|
->active();
|
|
|
|
// Search filter (name, SKU, description)
|
|
if ($search = $request->input('search')) {
|
|
$query->where(function ($q) use ($search) {
|
|
$q->where('name', 'ilike', "%{$search}%")
|
|
->orWhere('sku', 'ilike', "%{$search}%")
|
|
->orWhere('description', 'ilike', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
// Brand filter (supports multiple)
|
|
if ($brandIds = $request->input('brand_id')) {
|
|
$brandIds = is_array($brandIds) ? $brandIds : [$brandIds];
|
|
$query->whereIn('brand_id', $brandIds);
|
|
}
|
|
|
|
// Category filter (uses category_id foreign key)
|
|
if ($categoryId = $request->input('category_id')) {
|
|
$query->where('category_id', $categoryId);
|
|
}
|
|
|
|
// Strain type filter
|
|
if ($strainType = $request->input('strain_type')) {
|
|
$query->whereHas('strain', function ($q) use ($strainType) {
|
|
$q->where('type', $strainType);
|
|
});
|
|
}
|
|
|
|
// Price range filter
|
|
if ($priceMin = $request->input('price_min')) {
|
|
$query->where('wholesale_price', '>=', $priceMin);
|
|
}
|
|
if ($priceMax = $request->input('price_max')) {
|
|
$query->where('wholesale_price', '<=', $priceMax);
|
|
}
|
|
|
|
// In stock filter
|
|
if ($request->input('in_stock')) {
|
|
$query->where('quantity_on_hand', '>', 0);
|
|
}
|
|
|
|
// Sorting
|
|
$sort = $request->input('sort', 'newest');
|
|
match ($sort) {
|
|
'name_asc' => $query->orderBy('name', 'asc'),
|
|
'name_desc' => $query->orderBy('name', 'desc'),
|
|
'price_asc' => $query->orderBy('wholesale_price', 'asc'),
|
|
'price_desc' => $query->orderBy('wholesale_price', 'desc'),
|
|
'newest' => $query->latest(),
|
|
default => $query->latest(),
|
|
};
|
|
|
|
// View mode (grid/list)
|
|
$viewMode = $request->input('view', 'grid');
|
|
|
|
// Paginate results
|
|
$perPage = $viewMode === 'list' ? 10 : 12;
|
|
$products = $query->paginate($perPage)->withQueryString();
|
|
|
|
// Get brands with product counts for faceted filter
|
|
$brands = Brand::query()
|
|
->active()
|
|
->withCount(['products' => fn ($q) => $q->active()])
|
|
->orderBy('name')
|
|
->get()
|
|
->filter(fn ($b) => $b->products_count > 0);
|
|
|
|
// Get categories with product counts
|
|
$categories = ProductCategory::query()
|
|
->whereNull('parent_id') // Only top-level categories
|
|
->where('is_active', true)
|
|
->withCount(['products' => fn ($q) => $q->active()])
|
|
->get()
|
|
->filter(fn ($c) => $c->products_count > 0)
|
|
->sortByDesc('products_count');
|
|
|
|
// Featured products for hero carousel
|
|
$featuredProducts = Product::query()
|
|
->with(['brand', 'strain'])
|
|
->featured()
|
|
->inStock()
|
|
->limit(5)
|
|
->get();
|
|
|
|
// Top brands (by product count) for homepage section
|
|
$topBrands = Brand::query()
|
|
->active()
|
|
->withCount(['products' => fn ($q) => $q->active()])
|
|
->get()
|
|
->filter(fn ($b) => $b->products_count > 0)
|
|
->sortByDesc('products_count')
|
|
->take(6);
|
|
|
|
// New arrivals (products created in last 14 days)
|
|
$newArrivals = Product::query()
|
|
->with(['brand', 'strain', 'category'])
|
|
->active()
|
|
->inStock()
|
|
->where('created_at', '>=', now()->subDays(14))
|
|
->orderByDesc('created_at')
|
|
->limit(8)
|
|
->get();
|
|
|
|
// Trending products (most ordered in last 30 days - simplified query)
|
|
$trendingIds = DB::table('order_items')
|
|
->select('product_id', DB::raw('SUM(quantity) as total_sold'))
|
|
->where('created_at', '>=', now()->subDays(30))
|
|
->groupBy('product_id')
|
|
->orderByDesc('total_sold')
|
|
->limit(8)
|
|
->pluck('product_id');
|
|
|
|
$trending = $trendingIds->isNotEmpty()
|
|
? Product::with(['brand', 'strain', 'category'])
|
|
->whereIn('id', $trendingIds)
|
|
->active()
|
|
->get()
|
|
->sortBy(fn ($p) => array_search($p->id, $trendingIds->toArray()))
|
|
: collect();
|
|
|
|
// Active filters for pills display
|
|
$activeFilters = collect([
|
|
'search' => $request->input('search'),
|
|
'brand_id' => $request->input('brand_id'),
|
|
'category_id' => $request->input('category_id'),
|
|
'strain_type' => $request->input('strain_type'),
|
|
'in_stock' => $request->input('in_stock'),
|
|
])->filter();
|
|
|
|
return view('buyer.marketplace.index', compact(
|
|
'products',
|
|
'brands',
|
|
'categories',
|
|
'featuredProducts',
|
|
'topBrands',
|
|
'newArrivals',
|
|
'trending',
|
|
'business',
|
|
'viewMode',
|
|
'activeFilters',
|
|
'hasFilters'
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Display all products (redirects to main browse page)
|
|
*/
|
|
public function products()
|
|
{
|
|
return redirect()->route('buyer.browse');
|
|
}
|
|
|
|
/**
|
|
* Display all brands directory
|
|
*/
|
|
public function brands()
|
|
{
|
|
$brands = Brand::query()
|
|
->active()
|
|
->withCount(['products' => function ($query) {
|
|
$query->active();
|
|
}])
|
|
->orderBy('name')
|
|
->get();
|
|
|
|
$business = auth()->user()->businesses->first();
|
|
|
|
return view('buyer.marketplace.brands', compact('brands', 'business'));
|
|
}
|
|
|
|
/**
|
|
* Display products in specific category
|
|
*/
|
|
public function category($category)
|
|
{
|
|
$business = auth()->user()->businesses->first();
|
|
|
|
return view('buyer.marketplace.category', compact('category', 'business'));
|
|
}
|
|
|
|
/**
|
|
* Show individual product (nested under brand)
|
|
*/
|
|
public function showProduct($brandSlug, $productSlug)
|
|
{
|
|
// Find brand by slug
|
|
$brand = Brand::query()
|
|
->where('slug', $brandSlug)
|
|
->active()
|
|
->firstOrFail();
|
|
|
|
// Find product by slug within this brand
|
|
$product = Product::query()
|
|
->with([
|
|
'brand',
|
|
'strain',
|
|
'availableBatches' => function ($query) {
|
|
$query->with(['coaFiles'])
|
|
->orderBy('production_date', 'desc')
|
|
->orderBy('created_at', 'desc');
|
|
},
|
|
])
|
|
->where('brand_id', $brand->id)
|
|
->where(function ($query) use ($productSlug) {
|
|
$query->where('slug', $productSlug);
|
|
// Only try ID lookup if the value is numeric
|
|
if (is_numeric($productSlug)) {
|
|
$query->orWhere('id', $productSlug);
|
|
}
|
|
})
|
|
->active()
|
|
->firstOrFail();
|
|
|
|
// Get related products from same brand
|
|
$relatedProducts = Product::query()
|
|
->with(['brand', 'strain'])
|
|
->where('brand_id', $product->brand_id)
|
|
->where('id', '!=', $product->id)
|
|
->active()
|
|
->inStock()
|
|
->limit(4)
|
|
->get();
|
|
|
|
$business = auth()->user()->businesses->first();
|
|
|
|
return view('buyer.marketplace.product', compact('product', 'relatedProducts', 'brand', 'business'));
|
|
}
|
|
|
|
/**
|
|
* Show individual brand storefront
|
|
*/
|
|
public function showBrand($brandSlug)
|
|
{
|
|
// Find brand by slug
|
|
$brand = Brand::query()
|
|
->where('slug', $brandSlug)
|
|
->active()
|
|
->firstOrFail();
|
|
|
|
// Get featured products from this brand
|
|
$featuredProducts = Product::query()
|
|
->with(['strain', 'brand'])
|
|
->where('brand_id', $brand->id)
|
|
->featured()
|
|
->inStock()
|
|
->limit(3)
|
|
->get();
|
|
|
|
// Get all products from this brand
|
|
$products = Product::query()
|
|
->with(['strain', 'brand'])
|
|
->where('brand_id', $brand->id)
|
|
->active()
|
|
->orderBy('is_featured', 'desc')
|
|
->orderBy('name')
|
|
->paginate(20);
|
|
|
|
$business = auth()->user()->businesses->first();
|
|
|
|
return view('buyer.marketplace.brand', compact('brand', 'featuredProducts', 'products', 'business'));
|
|
}
|
|
}
|