Compare commits
2 Commits
fixing-dai
...
feature/ma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
829dbe0c38 | ||
|
|
3689d07831 |
@@ -80,8 +80,8 @@ class BusinessResource extends Resource
|
||||
->required()
|
||||
->options([
|
||||
'retailer' => 'Retailer/Dispensary',
|
||||
'brand' => 'Brand/Manufacturer',
|
||||
'both' => 'Both Retailer & Brand',
|
||||
'brand' => 'Seller/Manufacturer',
|
||||
'both' => 'Both Retailer & Seller',
|
||||
])
|
||||
->default('retailer'),
|
||||
TextInput::make('business_group')
|
||||
@@ -717,7 +717,7 @@ class BusinessResource extends Resource
|
||||
->label('Business Type')
|
||||
->options([
|
||||
'retailer' => 'Retailer/Dispensary',
|
||||
'brand' => 'Brand/Manufacturer',
|
||||
'brand' => 'Seller/Manufacturer',
|
||||
'both' => 'Both',
|
||||
]),
|
||||
TernaryFilter::make('is_active')
|
||||
|
||||
@@ -11,6 +11,7 @@ use Filament\Actions\ActionGroup;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
@@ -85,6 +86,21 @@ class UserResource extends Resource
|
||||
'suspended' => 'Suspended',
|
||||
])
|
||||
->default('active'),
|
||||
TextInput::make('password')
|
||||
->password()
|
||||
->dehydrateStateUsing(fn ($state) => bcrypt($state))
|
||||
->required(fn ($livewire) => $livewire instanceof \Filament\Resources\Pages\CreateRecord)
|
||||
->label('Password')
|
||||
->maxLength(255)
|
||||
->visible(fn ($livewire) => $livewire instanceof \Filament\Resources\Pages\CreateRecord),
|
||||
TextInput::make('password_confirmation')
|
||||
->password()
|
||||
->dehydrated(false)
|
||||
->required(fn ($livewire) => $livewire instanceof \Filament\Resources\Pages\CreateRecord)
|
||||
->label('Confirm Password')
|
||||
->same('password')
|
||||
->maxLength(255)
|
||||
->visible(fn ($livewire) => $livewire instanceof \Filament\Resources\Pages\CreateRecord),
|
||||
])->columns(2),
|
||||
|
||||
Section::make('Business Association')
|
||||
|
||||
@@ -33,7 +33,12 @@ class UnifiedAuthenticatedSessionController extends Controller
|
||||
// Smart routing based on user type
|
||||
switch ($user->user_type) {
|
||||
case 'buyer':
|
||||
return redirect()->route('buyer.dashboard');
|
||||
// Redirect to business dashboard if they have a business
|
||||
$business = $user->primaryBusiness();
|
||||
if ($business) {
|
||||
return redirect()->route('buyer.business.dashboard', $business);
|
||||
}
|
||||
return redirect()->route('buyer.setup');
|
||||
|
||||
case 'seller':
|
||||
return redirect()->route('seller.dashboard');
|
||||
|
||||
@@ -125,6 +125,16 @@ class CheckoutController extends Controller
|
||||
$tax = ($subtotal + $surcharge) * $taxRate;
|
||||
$total = $subtotal + $surcharge + $tax;
|
||||
|
||||
// TODO: FUTURE FEATURE - Order Splitting by Brand
|
||||
// Currently, one order is created per checkout with all cart items.
|
||||
// Future implementation should:
|
||||
// 1. Group cart items by brand_id
|
||||
// 2. Create one separate order per brand (multiple orders from single cart)
|
||||
// 3. Each seller receives only their brand's order
|
||||
// 4. Buyer sees multiple orders in their order history (one per brand)
|
||||
// 5. Update this transaction to loop through grouped items and create multiple orders
|
||||
// Reference: Similar to LeafLink's multi-vendor checkout behavior
|
||||
|
||||
// Create order in transaction
|
||||
$order = DB::transaction(function () use ($request, $user, $business, $items, $subtotal, $surcharge, $tax, $total, $paymentTerms, $dueDate) {
|
||||
// Generate order number
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Business;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Order;
|
||||
use App\Services\CartService;
|
||||
@@ -10,14 +11,12 @@ class BuyerDashboardController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the buyer marketplace dashboard
|
||||
* Now requires business context in URL
|
||||
*/
|
||||
public function index(CartService $cartService)
|
||||
public function index(Business $business, CartService $cartService)
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
// Get user's primary business
|
||||
$business = $user->primaryBusiness();
|
||||
|
||||
// Check if user needs to complete onboarding
|
||||
$needsOnboarding = ! $user->business_onboarding_completed
|
||||
|| ($business && in_array($business->status, ['not_started', 'in_progress', 'rejected']));
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Brand;
|
||||
use App\Models\Business;
|
||||
use App\Models\Product;
|
||||
use App\Models\Strain;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -12,7 +13,7 @@ class MarketplaceController extends Controller
|
||||
/**
|
||||
* Display marketplace browse page
|
||||
*/
|
||||
public function index(Request $request)
|
||||
public function index(Business $business, Request $request)
|
||||
{
|
||||
// Start with active products only
|
||||
$query = Product::query()
|
||||
@@ -78,7 +79,7 @@ class MarketplaceController extends Controller
|
||||
->limit(3)
|
||||
->get();
|
||||
|
||||
return view('buyer.marketplace.index', compact('products', 'brands', 'featuredProducts'));
|
||||
return view('buyer.marketplace.index', compact('business', 'products', 'brands', 'featuredProducts'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,7 +93,7 @@ class MarketplaceController extends Controller
|
||||
/**
|
||||
* Display all brands directory
|
||||
*/
|
||||
public function brands()
|
||||
public function brands(Business $business)
|
||||
{
|
||||
$brands = Brand::query()
|
||||
->active()
|
||||
@@ -102,29 +103,30 @@ class MarketplaceController extends Controller
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('buyer.marketplace.brands', compact('brands'));
|
||||
return view('buyer.marketplace.brands', compact('brands', 'business'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display products in specific category
|
||||
*/
|
||||
public function category($category)
|
||||
public function category(Business $business, $category)
|
||||
{
|
||||
return view('buyer.marketplace.category', compact('category'));
|
||||
return view('buyer.marketplace.category', compact('business', 'category'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show individual product (nested under brand)
|
||||
* Now uses brand_code for URLs (e.g., /brands/2GDWH/123)
|
||||
*/
|
||||
public function showProduct($brandSlug, $productSlug)
|
||||
public function showProduct(Business $business, Brand $brand, $productId)
|
||||
{
|
||||
// Find brand by slug
|
||||
$brand = Brand::query()
|
||||
->where('slug', $brandSlug)
|
||||
->active()
|
||||
->firstOrFail();
|
||||
// Brand is automatically resolved via route model binding using brand_code
|
||||
// Verify brand is active
|
||||
if (!$brand->is_active) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
// Find product by slug within this brand
|
||||
// Find product by ID within this brand
|
||||
$product = Product::query()
|
||||
->with([
|
||||
'brand',
|
||||
@@ -138,14 +140,8 @@ class MarketplaceController extends Controller
|
||||
->orderBy('created_at', 'desc');
|
||||
},
|
||||
])
|
||||
->where('id', $productId)
|
||||
->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();
|
||||
|
||||
@@ -159,19 +155,20 @@ class MarketplaceController extends Controller
|
||||
->limit(4)
|
||||
->get();
|
||||
|
||||
return view('buyer.marketplace.product', compact('product', 'relatedProducts', 'brand'));
|
||||
return view('buyer.marketplace.product', compact('business', 'product', 'relatedProducts', 'brand'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show individual brand storefront
|
||||
* Now uses brand_code (e.g., /brands/2GDWH)
|
||||
*/
|
||||
public function showBrand($brandSlug)
|
||||
public function showBrand(Business $business, Brand $brand)
|
||||
{
|
||||
// Find brand by slug
|
||||
$brand = Brand::query()
|
||||
->where('slug', $brandSlug)
|
||||
->active()
|
||||
->firstOrFail();
|
||||
// Brand is automatically resolved via route model binding using brand_code
|
||||
// Verify brand is active
|
||||
if (!$brand->is_active) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
// Get featured products from this brand
|
||||
$featuredProducts = Product::query()
|
||||
@@ -191,6 +188,6 @@ class MarketplaceController extends Controller
|
||||
->orderBy('name')
|
||||
->paginate(20);
|
||||
|
||||
return view('buyer.marketplace.brand', compact('brand', 'featuredProducts', 'products'));
|
||||
return view('buyer.marketplace.brand', compact('business', 'brand', 'featuredProducts', 'products'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,15 @@ class Brand extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
/**
|
||||
* Get the route key name for Laravel route model binding.
|
||||
* Uses brand_code for buyer-facing URLs (e.g., /b/imabuyer/brands/2GDWH)
|
||||
*/
|
||||
public function getRouteKeyName(): string
|
||||
{
|
||||
return 'brand_code';
|
||||
}
|
||||
|
||||
// Product Categories that can be organized under brands
|
||||
public const PRODUCT_CATEGORIES = [
|
||||
'flower' => 'Flower',
|
||||
@@ -33,6 +42,7 @@ class Brand extends Model
|
||||
// Brand Identity
|
||||
'name',
|
||||
'slug',
|
||||
'brand_code', // 5-char alphanumeric code for buyer URLs
|
||||
'sku_prefix', // SKU prefix for products
|
||||
'description',
|
||||
'tagline',
|
||||
@@ -166,14 +176,6 @@ class Brand extends Model
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get route key (slug for URLs)
|
||||
*/
|
||||
public function getRouteKeyName(): string
|
||||
{
|
||||
return 'slug';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate slug from name
|
||||
*/
|
||||
|
||||
@@ -27,6 +27,15 @@ class Business extends Model implements AuditableContract
|
||||
return ['uuid'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the route key name for Laravel route model binding.
|
||||
* This ensures URLs use slug instead of ID (e.g., /b/imabuyer/browse instead of /b/4/browse)
|
||||
*/
|
||||
public function getRouteKeyName(): string
|
||||
{
|
||||
return 'slug';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new UUID for the model (first 16 hex chars = 18 total with hyphens).
|
||||
*/
|
||||
@@ -46,8 +55,10 @@ class Business extends Model implements AuditableContract
|
||||
];
|
||||
|
||||
// Business types aligned with cannabis industry (stored in business_type column)
|
||||
// NOTE: 'brand' here means seller/manufacturer business (not the Brand model)
|
||||
// A business with type 'brand' can own multiple Brand records in the brands table
|
||||
public const BUSINESS_TYPES = [
|
||||
'brand' => 'Brand/Manufacturer',
|
||||
'brand' => 'Seller/Manufacturer', // Businesses that sell brands/products
|
||||
'retailer' => 'Retailer/Dispensary',
|
||||
'distributor' => 'Distributor',
|
||||
'cultivator' => 'Cultivator',
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('brands', function (Blueprint $table) {
|
||||
$table->string('brand_code', 5)->nullable()->unique()->after('slug');
|
||||
$table->index('brand_code');
|
||||
});
|
||||
|
||||
// Generate brand codes for existing brands
|
||||
$this->generateBrandCodes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('brands', function (Blueprint $table) {
|
||||
$table->dropIndex(['brand_code']);
|
||||
$table->dropColumn('brand_code');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique 5-character alphanumeric codes for existing brands
|
||||
* Excludes confusing characters: 0, O, 1, L, I
|
||||
*/
|
||||
private function generateBrandCodes(): void
|
||||
{
|
||||
$brands = DB::table('brands')->whereNull('brand_code')->get();
|
||||
|
||||
foreach ($brands as $brand) {
|
||||
$code = $this->generateUniqueCode();
|
||||
DB::table('brands')->where('id', $brand->id)->update(['brand_code' => $code]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique 5-character code
|
||||
* Character set: 23456789ABCDEFGHJKMNPQRSTUVWXYZ (excludes 0, O, 1, L, I)
|
||||
*/
|
||||
private function generateUniqueCode(): string
|
||||
{
|
||||
$characters = '23456789ABCDEFGHJKMNPQRSTUVWXYZ';
|
||||
$maxAttempts = 100;
|
||||
|
||||
for ($attempt = 0; $attempt < $maxAttempts; $attempt++) {
|
||||
$code = '';
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$code .= $characters[random_int(0, strlen($characters) - 1)];
|
||||
}
|
||||
|
||||
// Check if code is unique
|
||||
$exists = DB::table('brands')->where('brand_code', $code)->exists();
|
||||
if (!$exists) {
|
||||
return $code;
|
||||
}
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Could not generate unique brand code after ' . $maxAttempts . ' attempts');
|
||||
}
|
||||
};
|
||||
@@ -1,3 +1,7 @@
|
||||
@php
|
||||
$business = $business ?? auth()->user()->primaryBusiness();
|
||||
@endphp
|
||||
|
||||
@extends('layouts.buyer-app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
@@ -6,7 +10,7 @@
|
||||
<div class="breadcrumbs text-sm mb-6">
|
||||
<ul>
|
||||
<li><a href="{{ route('buyer.dashboard') }}">Dashboard</a></li>
|
||||
<li><a href="{{ route('buyer.brands.index') }}">Brands</a></li>
|
||||
<li><a href="{{ route('buyer.business.brands.index', $business) }}">Brands</a></li>
|
||||
<li class="opacity-80">{{ $brand->name }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -97,7 +101,7 @@
|
||||
|
||||
<!-- Product Info -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<a href="{{ route('buyer.brands.products.show', [$brand->slug, $product->slug ?? $product->id]) }}" class="font-semibold text-sm hover:text-primary line-clamp-2">
|
||||
<a href="{{ route('buyer.business.brands.products.show', [$business, $brand->id, $product->id]) }}" class="font-semibold text-sm hover:text-primary line-clamp-2">
|
||||
{{ $product->name }}
|
||||
</a>
|
||||
|
||||
@@ -160,7 +164,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold">
|
||||
<a href="{{ route('buyer.brands.products.show', [$brand->slug, $product->slug ?? $product->id]) }}" class="hover:text-primary">
|
||||
<a href="{{ route('buyer.business.brands.products.show', [$business, $brand->id, $product->id]) }}" class="hover:text-primary">
|
||||
{{ $product->name }}
|
||||
</a>
|
||||
</div>
|
||||
@@ -210,7 +214,7 @@
|
||||
<!-- Actions -->
|
||||
<td class="text-right">
|
||||
<div class="flex gap-2 justify-end">
|
||||
<a href="{{ route('buyer.brands.products.show', [$brand->slug, $product->slug ?? $product->id]) }}" class="btn btn-ghost btn-sm">
|
||||
<a href="{{ route('buyer.business.brands.products.show', [$business, $brand->id, $product->id]) }}" class="btn btn-ghost btn-sm">
|
||||
<span class="icon-[lucide--eye] size-4"></span>
|
||||
</a>
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
@php
|
||||
$business = $business ?? auth()->user()->primaryBusiness();
|
||||
@endphp
|
||||
|
||||
@extends('layouts.buyer-app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
@@ -12,7 +16,7 @@
|
||||
@if($brands->count() > 0)
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
@foreach($brands as $brand)
|
||||
<a href="{{ route('buyer.brands.show', $brand->slug) }}" class="card bg-base-100 shadow-lg hover:shadow-xl transition-all hover:-translate-y-1 duration-300">
|
||||
<a href="{{ route('buyer.business.brands.show', [$business, $brand->id]) }}" class="card bg-base-100 shadow-lg hover:shadow-xl transition-all hover:-translate-y-1 duration-300">
|
||||
<div class="card-body">
|
||||
<!-- Brand Logo -->
|
||||
<div class="flex items-center justify-center h-32 mb-4">
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
@php
|
||||
$business = $business ?? auth()->user()->primaryBusiness();
|
||||
@endphp
|
||||
|
||||
@extends('layouts.buyer-app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
@@ -44,7 +48,7 @@
|
||||
<span class="badge badge-lg bg-white/20 backdrop-blur-sm text-white border-white/30">THC {{ $featured->thc_percentage }}%</span>
|
||||
@endif
|
||||
</div>
|
||||
<a href="{{ route('buyer.brands.products.show', [$featured->brand->slug, $featured->slug ?? $featured->id]) }}" class="btn btn-warning btn-lg">
|
||||
<a href="{{ route('buyer.business.brands.products.show', [$business, $featured->brand, $featured->id]) }}" class="btn btn-warning btn-lg">
|
||||
<span class="icon-[lucide--shopping-cart] size-5"></span>
|
||||
Shop Now
|
||||
</a>
|
||||
@@ -105,7 +109,7 @@
|
||||
<div class="card-body">
|
||||
<h3 class="font-bold text-lg mb-4">Filters</h3>
|
||||
|
||||
<form method="GET" action="{{ route('buyer.browse') }}" id="filterForm">
|
||||
<form method="GET" action="{{ route('buyer.business.browse', $business) }}" id="filterForm">
|
||||
<!-- Search -->
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
@@ -193,7 +197,7 @@
|
||||
<span class="icon-[lucide--filter] size-4"></span>
|
||||
Apply
|
||||
</button>
|
||||
<a href="{{ route('buyer.browse') }}" class="btn btn-ghost">
|
||||
<a href="{{ route('buyer.business.browse', $business) }}" class="btn btn-ghost">
|
||||
<span class="icon-[lucide--x] size-4"></span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -213,7 +217,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Sort Dropdown -->
|
||||
<form method="GET" action="{{ route('buyer.browse') }}" class="flex items-center gap-2">
|
||||
<form method="GET" action="{{ route('buyer.business.browse', $business) }}" class="flex items-center gap-2">
|
||||
<!-- Preserve existing filters -->
|
||||
@foreach(request()->except('sort') as $key => $value)
|
||||
@if(is_array($value))
|
||||
@@ -278,7 +282,7 @@
|
||||
<!-- Brand -->
|
||||
@if($product->brand)
|
||||
<div class="text-xs font-semibold uppercase tracking-wide">
|
||||
<a href="{{ route('buyer.brands.show', $product->brand->slug) }}" class="text-primary hover:underline">
|
||||
<a href="{{ route('buyer.business.brands.show', [$business, $product->brand]) }}" class="text-primary hover:underline">
|
||||
{{ $product->brand->name }}
|
||||
</a>
|
||||
</div>
|
||||
@@ -286,7 +290,7 @@
|
||||
|
||||
<!-- Product Name -->
|
||||
<h3 class="card-title text-lg">
|
||||
<a href="{{ route('buyer.brands.products.show', [$product->brand->slug, $product->slug ?? $product->id]) }}" class="hover:text-primary">
|
||||
<a href="{{ route('buyer.business.brands.products.show', [$business, $product->brand, $product->id]) }}" class="hover:text-primary">
|
||||
{{ $product->name }}
|
||||
</a>
|
||||
</h3>
|
||||
@@ -330,7 +334,7 @@
|
||||
<!-- Actions -->
|
||||
<div class="card-actions justify-between items-center mt-4"
|
||||
x-data="{ productId: {{ $product->id }}, availableQty: {{ $product->available_quantity }} }">
|
||||
<a href="{{ route('buyer.brands.products.show', [$product->brand->slug, $product->slug ?? $product->id]) }}" class="btn btn-ghost btn-sm">
|
||||
<a href="{{ route('buyer.business.brands.products.show', [$business, $product->brand, $product->id]) }}" class="btn btn-ghost btn-sm">
|
||||
<span class="icon-[lucide--eye] size-4"></span>
|
||||
Details
|
||||
</a>
|
||||
@@ -390,7 +394,7 @@
|
||||
<span class="icon-[lucide--package-search] size-16 text-gray-300 mx-auto mb-4"></span>
|
||||
<h3 class="text-xl font-semibold text-gray-700 mb-2">No products found</h3>
|
||||
<p class="text-gray-500 mb-4">Try adjusting your filters or search terms</p>
|
||||
<a href="{{ route('buyer.browse') }}" class="btn btn-primary btn-sm">
|
||||
<a href="{{ route('buyer.business.browse', $business) }}" class="btn btn-primary btn-sm">
|
||||
Clear All Filters
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
@php
|
||||
$business = $business ?? auth()->user()->primaryBusiness();
|
||||
@endphp
|
||||
|
||||
@extends('layouts.buyer-app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
@@ -6,9 +10,9 @@
|
||||
<div class="breadcrumbs text-sm mb-6">
|
||||
<ul>
|
||||
<li><a href="{{ route('buyer.dashboard') }}">Dashboard</a></li>
|
||||
<li><a href="{{ route('buyer.brands.index') }}">Brands</a></li>
|
||||
<li><a href="{{ route('buyer.business.brands.index', $business) }}">Brands</a></li>
|
||||
@if($product->brand)
|
||||
<li><a href="{{ route('buyer.brands.show', $product->brand->slug) }}">{{ $product->brand->name }}</a></li>
|
||||
<li><a href="{{ route('buyer.business.brands.show', [$business, $product->brand]) }}">{{ $product->brand->name }}</a></li>
|
||||
@endif
|
||||
<li class="opacity-80">{{ $product->name }}</li>
|
||||
</ul>
|
||||
@@ -66,7 +70,7 @@
|
||||
<div>
|
||||
<!-- Brand -->
|
||||
@if($product->brand)
|
||||
<a href="{{ route('buyer.brands.show', $product->brand->slug) }}" class="text-sm text-primary font-semibold uppercase tracking-wide hover:underline">
|
||||
<a href="{{ route('buyer.business.brands.show', [$business, $product->brand]) }}" class="text-sm text-primary font-semibold uppercase tracking-wide hover:underline">
|
||||
{{ $product->brand->name }}
|
||||
</a>
|
||||
@endif
|
||||
@@ -383,7 +387,7 @@
|
||||
|
||||
<div class="card-body">
|
||||
<h4 class="card-title text-base">
|
||||
<a href="{{ route('buyer.brands.products.show', [$product->brand->slug, $relatedProduct->slug ?? $relatedProduct->id]) }}" class="hover:text-primary">
|
||||
<a href="{{ route('buyer.business.brands.products.show', [$business, $product->brand, $relatedProduct->id]) }}" class="hover:text-primary">
|
||||
{{ $relatedProduct->name }}
|
||||
</a>
|
||||
</h4>
|
||||
@@ -393,7 +397,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end mt-2">
|
||||
<a href="{{ route('buyer.brands.products.show', [$product->brand->slug, $relatedProduct->slug ?? $relatedProduct->id]) }}" class="btn btn-primary btn-sm">
|
||||
<a href="{{ route('buyer.business.brands.products.show', [$business, $product->brand, $relatedProduct->id]) }}" class="btn btn-primary btn-sm">
|
||||
View Details
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -10,9 +10,13 @@
|
||||
aria-label="Dense layout sidebar" />
|
||||
<div id="layout-sidebar-hover" class="bg-base-300 h-screen w-1"></div>
|
||||
|
||||
@php
|
||||
$business = auth()->user()->primaryBusiness();
|
||||
@endphp
|
||||
|
||||
<div id="layout-sidebar" class="sidebar-menu sidebar-menu-activation">
|
||||
<div class="flex min-h-16 items-center justify-center gap-3 px-4">
|
||||
<a href="{{ route('buyer.dashboard') }}">
|
||||
<a href="{{ $business ? route('buyer.business.dashboard', $business) : route('buyer.setup') }}">
|
||||
<img alt="logo" class="h-8 logo-img" src="{{ asset('images/canna_white.svg') }}" />
|
||||
</a>
|
||||
</div>
|
||||
@@ -56,7 +60,7 @@
|
||||
</div>
|
||||
<div class="collapse-content ms-6.5 !p-0">
|
||||
<div class="mt-0.5 space-y-0.5">
|
||||
<a class="menu-item {{ request()->routeIs('buyer.dashboard') ? 'active' : '' }}" href="{{ route('buyer.dashboard') }}">
|
||||
<a class="menu-item {{ request()->routeIs('buyer.business.dashboard') ? 'active' : '' }}" href="{{ $business ? route('buyer.business.dashboard', $business) : '#' }}">
|
||||
<span class="grow">Overview</span>
|
||||
</a>
|
||||
<a class="menu-item" href="#">
|
||||
@@ -74,7 +78,7 @@
|
||||
<span class="grow">Promotion</span>
|
||||
</a>
|
||||
|
||||
<a class="menu-item {{ request()->routeIs('buyer.browse*') ? 'active' : '' }}" href="{{ route('buyer.browse') }}">
|
||||
<a class="menu-item {{ request()->routeIs('buyer.business.browse*') ? 'active' : '' }}" href="{{ $business ? route('buyer.business.browse', $business) : '#' }}">
|
||||
<span class="icon-[lucide--store] size-4"></span>
|
||||
<span class="grow">Shop</span>
|
||||
</a>
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
use App\Http\Controllers\ProfileController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
// Custom route model binding for buyer business by slug
|
||||
Route::bind('business', function (string $value) {
|
||||
$business = \App\Models\Business::where('slug', $value)->firstOrFail();
|
||||
|
||||
// Verify user has access to this business
|
||||
if (! auth()->check() || ! auth()->user()->businesses->contains($business->id)) {
|
||||
abort(403, 'You do not have access to this business.');
|
||||
}
|
||||
|
||||
return $business;
|
||||
});
|
||||
|
||||
// Custom route model binding for orders by order number
|
||||
Route::bind('order', function (string $value) {
|
||||
return \App\Models\Order::where('order_number', $value)
|
||||
@@ -17,6 +29,32 @@ Route::bind('invoice', function (string $value) {
|
||||
|
||||
// Buyer-specific routes under /b/ prefix (NEW buyer functionality)
|
||||
Route::prefix('b')->name('buyer.')->middleware('buyer')->group(function () {
|
||||
// Root redirect - redirect /b/ to user's business dashboard
|
||||
Route::get('/', function () {
|
||||
$business = auth()->user()?->primaryBusiness();
|
||||
if ($business) {
|
||||
return redirect()->route('buyer.business.dashboard', $business);
|
||||
}
|
||||
return redirect()->route('buyer.setup');
|
||||
})->middleware(['auth', 'verified']);
|
||||
|
||||
// Legacy route redirects for backwards compatibility
|
||||
Route::get('/browse', function () {
|
||||
$business = auth()->user()?->primaryBusiness();
|
||||
if ($business) {
|
||||
return redirect()->route('buyer.business.browse', $business);
|
||||
}
|
||||
return redirect()->route('buyer.setup');
|
||||
})->middleware(['auth', 'verified']);
|
||||
|
||||
Route::get('/dashboard', function () {
|
||||
$business = auth()->user()?->primaryBusiness();
|
||||
if ($business) {
|
||||
return redirect()->route('buyer.business.dashboard', $business);
|
||||
}
|
||||
return redirect()->route('buyer.setup');
|
||||
})->middleware(['auth', 'verified'])->name('dashboard');
|
||||
|
||||
// Buyer registration routes (email-first flow)
|
||||
Route::middleware('guest')->group(function () {
|
||||
// Step 1: Email collection
|
||||
@@ -48,8 +86,17 @@ Route::prefix('b')->name('buyer.')->middleware('buyer')->group(function () {
|
||||
Route::post('/setup/{step?}', [\App\Http\Controllers\BusinessSetupController::class, 'store'])->name('setup.store')->where('step', '[1-4]');
|
||||
});
|
||||
|
||||
// Buyer browsing and dashboard (accessible without approval)
|
||||
// Buyer notifications (not business-scoped)
|
||||
Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::get('/notifications', [\App\Http\Controllers\Buyer\NotificationController::class, 'index'])->name('notifications.index');
|
||||
Route::get('/notifications/dropdown', [\App\Http\Controllers\Buyer\NotificationController::class, 'dropdown'])->name('notifications.dropdown');
|
||||
Route::get('/notifications/count', [\App\Http\Controllers\Buyer\NotificationController::class, 'count'])->name('notifications.count');
|
||||
Route::post('/notifications/{id}/read', [\App\Http\Controllers\Buyer\NotificationController::class, 'markAsRead'])->name('notifications.read');
|
||||
Route::post('/notifications/read-all', [\App\Http\Controllers\Buyer\NotificationController::class, 'markAllAsRead'])->name('notifications.read-all');
|
||||
});
|
||||
|
||||
// Business-scoped buyer routes (browsing and dashboard)
|
||||
Route::prefix('{business}')->name('business.')->middleware(['auth', 'verified'])->group(function () {
|
||||
// Main buyer dashboard (marketplace browsing)
|
||||
Route::get('/dashboard', [\App\Http\Controllers\BuyerDashboardController::class, 'index'])->name('dashboard');
|
||||
|
||||
@@ -58,17 +105,10 @@ Route::prefix('b')->name('buyer.')->middleware('buyer')->group(function () {
|
||||
Route::get('/browse/products', [\App\Http\Controllers\MarketplaceController::class, 'products'])->name('browse.products');
|
||||
Route::get('/browse/categories/{category}', [\App\Http\Controllers\MarketplaceController::class, 'category'])->name('browse.category');
|
||||
|
||||
// Brand directory and pages
|
||||
// Brand directory and pages (using 5-char brand codes: e.g., /brands/2GDWH)
|
||||
Route::get('/brands', [\App\Http\Controllers\MarketplaceController::class, 'brands'])->name('brands.index');
|
||||
Route::get('/brands/{brand}', [\App\Http\Controllers\MarketplaceController::class, 'showBrand'])->name('brands.show');
|
||||
Route::get('/brands/{brand}/{product}', [\App\Http\Controllers\MarketplaceController::class, 'showProduct'])->name('brands.products.show');
|
||||
|
||||
// Buyer Notification Routes
|
||||
Route::get('/notifications', [\App\Http\Controllers\Buyer\NotificationController::class, 'index'])->name('notifications.index');
|
||||
Route::get('/notifications/dropdown', [\App\Http\Controllers\Buyer\NotificationController::class, 'dropdown'])->name('notifications.dropdown');
|
||||
Route::get('/notifications/count', [\App\Http\Controllers\Buyer\NotificationController::class, 'count'])->name('notifications.count');
|
||||
Route::post('/notifications/{id}/read', [\App\Http\Controllers\Buyer\NotificationController::class, 'markAsRead'])->name('notifications.read');
|
||||
Route::post('/notifications/read-all', [\App\Http\Controllers\Buyer\NotificationController::class, 'markAllAsRead'])->name('notifications.read-all');
|
||||
Route::get('/brands/{brand}', [\App\Http\Controllers\MarketplaceController::class, 'showBrand'])->name('brands.show')->where('brand', '[23456789ABCDEFGHJKMNPQRSTUVWXYZ]{5}');
|
||||
Route::get('/brands/{brand}/{productId}', [\App\Http\Controllers\MarketplaceController::class, 'showProduct'])->name('brands.products.show')->where(['brand' => '[23456789ABCDEFGHJKMNPQRSTUVWXYZ]{5}', 'productId' => '[0-9]+']);
|
||||
});
|
||||
|
||||
// Buyer purchasing routes (require approval)
|
||||
|
||||
Reference in New Issue
Block a user