This commit is contained in:
2026-04-06 18:44:02 +05:00
commit d775fbb9ba
431 changed files with 31234 additions and 0 deletions

62
app/Models/Brand.php Normal file
View File

@@ -0,0 +1,62 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Brand extends Model
{
public $timestamps = false;
protected $fillable = [
'name',
'slug',
'description',
'logo',
'website',
'country',
'is_active',
];
protected $casts = [
'is_active' => 'boolean',
];
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
public function activeProducts(): HasMany
{
return $this->hasMany(Product::class)->where('is_active', true);
}
public function getProductsCountAttribute(): int
{
return $this->activeProducts()->count();
}
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public function scopePopular($query)
{
return $query->withCount('products')->orderBy('products_count', 'desc');
}
public function getLogoUrlAttribute(): string
{
if ($this->logo) {
$logoPath = 'assets/images/brands/' . $this->logo;
if (file_exists(public_path($logoPath))) {
return asset($logoPath);
}
}
return asset('assets/images/brands/default.svg');
}
}

97
app/Models/CartItem.php Normal file
View File

@@ -0,0 +1,97 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Auth;
class CartItem extends Model
{
protected $fillable = [
'user_id',
'session_id',
'variation_id',
'quantity',
'price'
];
protected $casts = [
'quantity' => 'integer',
'price' => 'decimal:2'
];
public function variation(): BelongsTo
{
return $this->belongsTo(ProductVariation::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function getTotalAttribute(): float
{
return $this->quantity * $this->price;
}
/**
* Получить корзину для текущего пользователя/сессии
*/
public static function getCartQuery()
{
return self::with('variation.product')
->where(function ($query) {
if (Auth::check()) {
$query->where('user_id', Auth::id());
} else {
$query->where('session_id', session()->getId());
}
});
}
/**
* Получить количество товаров в корзине
*/
public static function getCount()
{
return self::getCartQuery()->sum('quantity');
}
/**
* Получить общую сумму корзины
*/
public static function getTotal()
{
return self::getCartQuery()->get()->sum('total');
}
/**
* Получить товар в корзине по вариации
*/
public static function findByVariation($variationId)
{
return self::getCartQuery()
->where('variation_id', $variationId)
->first();
}
/**
* Получить товар в корзине по ID
*/
public static function findCartItem($id)
{
return self::getCartQuery()
->where('id', $id)
->first();
}
/**
* Очистить корзину
*/
public static function clear()
{
return self::getCartQuery()->delete();
}
}

243
app/Models/Category.php Normal file
View File

@@ -0,0 +1,243 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Category extends Model
{
public $timestamps = false;
protected $fillable = [
'name',
'slug',
'description',
'icon',
'image',
'parent_id',
'sort_order',
'is_active'
];
protected $casts = [
'is_active' => 'boolean',
];
public function parent(): BelongsTo
{
return $this->belongsTo(Category::class, 'parent_id');
}
public function children(): HasMany
{
return $this->hasMany(Category::class, 'parent_id')->orderBy('sort_order');
}
public function descendants()
{
return $this->children()->with('descendants');
}
public function products(): HasMany
{
return $this->hasMany(Product::class, 'category_id');
}
public function getFullSlugAttribute(): string
{
$slugs = [];
$current = $this;
while ($current) {
$slugs[] = $current->slug;
$current = $current->parent;
}
return implode('/', array_reverse($slugs));
}
public function getUrlAttribute(): string
{
return route('category.show', $this->full_slug);
}
public function getBreadcrumbsAttribute()
{
$breadcrumbs = [];
$current = $this;
while ($current) {
array_unshift($breadcrumbs, [
'name' => $current->name,
'slug' => $current->slug,
'url' => $current->url
]);
$current = $current->parent;
}
return $breadcrumbs;
}
public function getPathAttribute()
{
$path = [];
$current = $this;
while ($current) {
array_unshift($path, $current->slug);
$current = $current->parent;
}
return implode('/', $path);
}
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public function scopeRoot($query)
{
return $query->whereNull('parent_id');
}
public static function getTreeMenu()
{
return self::with('descendants')->root()->active()->orderBy('sort_order')->get();
}
public static function findByPath($path)
{
$slugs = explode('/', trim($path, '/'));
$category = null;
foreach ($slugs as $slug) {
if (!$category) {
$category = self::where('slug', $slug)
->whereNull('parent_id')
->active()
->first();
} else {
$category = $category->children()
->where('slug', $slug)
->active()
->first();
}
if (!$category) {
return null;
}
}
return $category;
}
public static function getSelectTree($maxLevel = 1)
{
$categories = self::with('parent')->get();
return $categories->map(function ($cat) {
$cat->level = $cat->calculateLevel();
return $cat;
})->filter(function ($cat) use ($maxLevel) {
return $cat->level < $maxLevel;
});
}
public static function getFlatTree()
{
$categories = self::with('parent')
->orderBy('parent_id')
->orderBy('sort_order')
->get();
return self::buildFlatTree($categories);
}
private static function buildFlatTree($categories, $parentId = null, $level = 0)
{
$result = collect();
foreach ($categories as $category) {
if ($category->parent_id == $parentId) {
$category->level = $level;
$result->push($category);
$children = self::buildFlatTree($categories, $category->id, $level + 1);
$result = $result->concat($children);
}
}
return $result;
}
public function getLevelAttribute()
{
return $this->calculateLevel();
}
public function calculateLevel($level = 0)
{
if ($this->parent) {
return $this->parent->calculateLevel($level + 1);
}
return $level;
}
public function isDescendantOf($ancestorId): bool
{
$current = $this;
while ($current) {
if ($current->id == $ancestorId) {
return true;
}
$current = $current->parent;
}
return false;
}
public function getIconUrlAttribute(): string
{
if ($this->icon !== NULL) {
$iconPath = 'assets/images/categories/icons/' . $this->icon;
if (file_exists(public_path($iconPath))) {
return asset($iconPath);
}
}
return asset('assets/images/categories/icons/default.svg');
}
public function getImageUrlAttribute(): string
{
if ($this->image !== NULL) {
$imagePath = 'assets/images/categories/images/' . $this->image;
if (file_exists(public_path($imagePath))) {
return asset($imagePath);
}
}
return asset('assets/images/categories/images/default.svg');
}
public function getAllCategoryIds(): array
{
$ids = [$this->id];
foreach ($this->children as $child) {
$ids = array_merge($ids, $child->getAllCategoryIds());
}
return $ids;
}
// В модели Category
public function productsWithChildren()
{
$categoryIds = $this->getAllCategoryIds();
return Product::whereIn('category_id', $categoryIds);
}
}

31
app/Models/Contact.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Contact extends Model
{
protected $fillable = [
'name',
'description',
'logo',
'favicon',
'phone',
'email',
'address',
'work_hours',
'telegram',
'whatsapp',
'vkontakte',
'meta_title',
'meta_description',
'meta_keywords',
];
public static function getData()
{
return self::first() ?? self::create();
}
}

203
app/Models/Order.php Normal file
View File

@@ -0,0 +1,203 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\DB;
class Order extends Model
{
protected $fillable = [
'order_number',
'user_id',
'customer_name',
'customer_email',
'customer_phone',
'shipping_address',
'subtotal',
'shipping_cost',
'discount',
'total',
'payment_method',
'payment_status',
'delivery_method',
'delivery_status',
'comment'
];
protected $casts = [
'subtotal' => 'decimal:2',
'shipping_cost' => 'decimal:2',
'discount' => 'decimal:2',
'total' => 'decimal:2'
];
const DELIVERY_STATUSES = [
'pending' => 'Ожидает обработки',
'processing' => 'В обработке',
'shipped' => 'Отправлен',
'delivered' => 'Доставлен',
'cancelled' => 'Отменён'
];
const PAYMENT_STATUSES = [
'pending' => 'Ожидает оплаты',
'paid' => 'Оплачен',
'failed' => 'Ошибка оплаты'
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function items(): HasMany
{
return $this->hasMany(OrderItem::class);
}
public static function generateOrderNumber(): string
{
return 'ORD-' . date('Ymd') . '-' . strtoupper(uniqid());
}
/**
* Создать заказ из корзины
*/
public static function createFromCart($data, $cartItems)
{
DB::beginTransaction();
try {
$subtotal = $cartItems->sum('total');
$shippingCost = self::calculateShipping($data['delivery_method']);
$total = $subtotal + $shippingCost;
$shippingAddress = null;
if (in_array($data['delivery_method'], ['courier', 'express'])) {
$shippingAddress = $data['shipping_address'];
}
$order = self::create([
'order_number' => self::generateOrderNumber(),
'user_id' => auth()->id(),
'customer_name' => $data['customer_name'],
'customer_email' => $data['customer_email'],
'customer_phone' => $data['customer_phone'],
'shipping_address' => $shippingAddress,
'subtotal' => $subtotal,
'shipping_cost' => $shippingCost,
'total' => $total,
'payment_method' => $data['payment_method'],
'delivery_method' => $data['delivery_method'],
'comment' => $data['comment'] ?? null
]);
foreach ($cartItems as $item) {
OrderItem::create([
'order_id' => $order->id,
'variation_id' => $item->variation_id,
'product_name' => $item->variation->product->name,
'variation_name' => $item->variation->name,
'sku' => $item->variation->sku,
'quantity' => $item->quantity,
'price' => $item->price,
'total' => $item->total
]);
$item->variation->decrement('stock', $item->quantity);
}
CartItem::clear();
DB::commit();
return ['success' => true, 'order' => $order];
} catch (\Exception $e) {
DB::rollBack();
return ['success' => false, 'message' => $e->getMessage()];
}
}
/**
* Рассчитать стоимость доставки
*/
private static function calculateShipping($method)
{
return match ($method) {
'express' => 300,
'courier' => 0,
'pickup' => 0,
default => 0,
};
}
public function getDeliveryStatusNameAttribute(): string
{
return self::DELIVERY_STATUSES[$this->delivery_status] ?? $this->delivery_status;
}
public function getPaymentStatusNameAttribute(): string
{
return self::PAYMENT_STATUSES[$this->payment_status] ?? $this->payment_status;
}
public function getDeliveryStatusColorAttribute(): string
{
return match ($this->delivery_status) {
'pending' => 'warning',
'processing' => 'info',
'shipped' => 'primary',
'delivered' => 'success',
'cancelled' => 'danger',
default => 'secondary'
};
}
public function getDeliveryStatusIconAttribute(): string
{
return match ($this->delivery_status) {
'pending' => 'bi-clock',
'processing' => 'bi-arrow-repeat',
'shipped' => 'bi-truck',
'delivered' => 'bi-check-circle',
'cancelled' => 'bi-x-circle',
default => 'bi-question-circle'
};
}
/**
* Получить все возможные статусы доставки для выпадающего списка
*/
public static function getDeliveryStatuses(): array
{
return self::DELIVERY_STATUSES;
}
/**
* Получить все возможные статусы оплаты для выпадающего списка
*/
public static function getPaymentStatuses(): array
{
return self::PAYMENT_STATUSES;
}
public static function getRevenue($startDate, $endDate, $paidOnly = true)
{
$query = self::query();
if ($paidOnly) {
$query->where('payment_status', 'paid');
}
return $query->whereBetween('created_at', [$startDate, $endDate])
->sum('total');
}
public static function getMonthlyRevenue()
{
return self::getRevenue(now()->startOfMonth(), now()->endOfMonth());
}
}

65
app/Models/OrderItem.php Normal file
View File

@@ -0,0 +1,65 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\DB;
class OrderItem extends Model
{
protected $fillable = [
'order_id',
'variation_id',
'product_name',
'variation_name',
'sku',
'quantity',
'price',
'total'
];
protected $casts = [
'quantity' => 'integer',
'price' => 'decimal:2',
'total' => 'decimal:2'
];
public function order(): BelongsTo
{
return $this->belongsTo(Order::class);
}
public function variation(): BelongsTo
{
return $this->belongsTo(ProductVariation::class);
}
public static function createFromCartItem($orderId, $cartItem)
{
return self::create([
'order_id' => $orderId,
'variation_id' => $cartItem->variation_id,
'product_name' => $cartItem->variation->product->name,
'variation_name' => $cartItem->variation->name,
'sku' => $cartItem->variation->sku,
'quantity' => $cartItem->quantity,
'price' => $cartItem->price,
'total' => $cartItem->total
]);
}
public function scopePopular($query, $limit = 5)
{
return $query->select(
'product_name',
'variation_name',
'variation_id',
DB::raw('SUM(quantity) as total_quantity'),
DB::raw('SUM(total) as total_revenue')
)
->groupBy('product_name', 'variation_name', 'variation_id')
->orderBy('total_quantity', 'desc')
->limit($limit);
}
}

28
app/Models/Permission.php Normal file
View File

@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Permission extends Model
{
public $timestamps = false;
protected $fillable = [
'name',
'slug',
'group',
'description',
];
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class)->withTimestamps();
}
public function scopeInGroup($query, string $group)
{
$query->where('group', $group);
}
}

183
app/Models/Product.php Normal file
View File

@@ -0,0 +1,183 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Product extends Model
{
protected $fillable = [
'name',
'slug',
'description',
'brand_id',
'category_id',
'is_active',
'meta_title',
'meta_description',
'meta_keywords',
];
protected $casts = [
'is_active' => 'boolean',
];
public function brand(): BelongsTo
{
return $this->belongsTo(Brand::class);
}
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
public function variations(): HasMany
{
return $this->hasMany(ProductVariation::class);
}
public function attributes(): HasMany
{
return $this->hasMany(ProductAttribute::class);
}
public function getDefaultVariationAttribute()
{
$defaultInStock = $this->variations->firstWhere(function ($variation) {
return $variation->is_default && $variation->stock > 0;
});
if ($defaultInStock) {
return $defaultInStock;
}
$anyInStock = $this->variations->firstWhere('stock', '>', 0);
if ($anyInStock) {
return $anyInStock;
}
return $this->variations->firstWhere('is_default', true) ?? $this->variations->first();
}
/**
* Получить URL главного изображения товара (из дефолтной вариации)
*/
public function getMainImageAttribute(): string
{
$defaultVariation = $this->default_variation;
if ($defaultVariation) {
return $defaultVariation->main_image_url;
}
return asset('assets/images/products/default.svg');
}
/**
* Получить изображения дефолтной вариации
*/
public function getDefaultVariationImages()
{
$defaultVariation = $this->default_variation;
if ($defaultVariation) {
return $defaultVariation->images;
}
return collect();
}
public static function getForCarousel($categoryId = null, $limit = 9, $itemsPerSlide = 3)
{
$query = self::with(['category', 'brand', 'variations'])
->active();
if ($categoryId) {
$query->byCategory($categoryId);
}
$products = $query->limit($limit)->get();
$products = $products->filter(function ($product) {
return $product->variations->isNotEmpty();
});
$products = $products->sortByDesc(function ($product) {
$defaultVariation = $product->default_variation;
if ($defaultVariation && $defaultVariation->stock > 0) {
return 3;
} elseif ($defaultVariation) {
return 2;
} elseif ($product->variations->firstWhere('stock', '>', 0)) {
return 1;
}
return 0;
});
return $products->chunk($itemsPerSlide);
}
public function getRatingAttribute(): array
{
return [
'score' => 4.9,
'count' => 1000
];
}
public function getMinPriceAttribute(): float
{
return $this->variations->min('price') ?? 0;
}
public function getMaxPriceAttribute(): float
{
return $this->variations->max('price') ?? 0;
}
public function getPriceRangeAttribute(): string
{
$min = $this->min_price;
$max = $this->max_price;
if ($min == $max) {
return number_format($min, 0, '.', ' ') . ' ₽';
}
return 'от ' . number_format($min, 0, '.', ' ') . ' ₽ до ' . number_format($max, 0, '.', ' ') . ' ₽';
}
public function getTotalStockAttribute(): int
{
return $this->variations->sum('stock');
}
public function getHasStockAttribute(): bool
{
return $this->total_stock > 0;
}
public function getFullNameAttribute(): string
{
return $this->brand ? "[{$this->brand->name}] {$this->name}" : $this->name;
}
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public function scopeByBrand($query, $brandId)
{
return $query->where('brand_id', $brandId);
}
public function scopeByCategory($query, $categoryId)
{
return $query->where('category_id', $categoryId);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ProductAttribute extends Model
{
protected $fillable = ['product_id', 'key', 'value'];
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
}
}

View File

@@ -0,0 +1,160 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class ProductVariation extends Model
{
protected $table = 'product_variations';
protected $fillable = [
'product_id',
'sku',
'name',
'price',
'old_price',
'stock',
'is_default',
'is_active',
];
protected $casts = [
'is_default' => 'boolean',
'is_active' => 'boolean',
'price' => 'float',
'old_price' => 'float',
];
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
}
public function attributes(): HasMany
{
return $this->hasMany(VariationAttribute::class, 'variation_id');
}
public function images(): HasMany
{
return $this->hasMany(VariationImage::class, 'variation_id')
->ordered();
}
/**
* Получить URL главного изображения вариации
*/
public function getMainImageUrlAttribute(): string
{
$image = $this->images()->first();
if ($image) {
return $image->url;
}
return asset('assets/images/products/default.svg');
}
/**
* Получить все изображения вариации
*/
public function getAllImages()
{
return $this->images;
}
public function getHasDiscountAttribute(): bool
{
return $this->old_price && $this->old_price > $this->price;
}
public function getDiscountPercentAttribute(): int
{
if (!$this->has_discount) return 0;
return round(($this->old_price - $this->price) / $this->old_price * 100);
}
public function getInStockAttribute(): bool
{
return $this->stock > 0;
}
public function getFormattedPriceAttribute(): string
{
return number_format($this->price, 0, '.', ' ') . ' ₽';
}
public function getFormattedOldPriceAttribute(): string
{
if (!$this->old_price) return '';
return number_format($this->old_price, 0, '.', ' ') . ' ₽';
}
public static function isSkuUnique($sku, $excludeId = null, $productId = null)
{
$query = self::where('sku', $sku);
if ($excludeId) {
$query->where('id', '!=', $excludeId);
}
if ($productId) {
$query->where('product_id', $productId);
}
return !$query->exists();
}
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public function scopeInStock($query)
{
return $query->where('stock', '>', 0);
}
public function scopeDiscounted($query)
{
return $query->whereNotNull('old_price')
->whereColumn('old_price', '>', 'price');
}
public function scopeDefault($query)
{
return $query->where('is_default', true);
}
// ========== МЕТОДЫ ДЛЯ AJAX ==========
public static function getImagesByVariationId($variationId)
{
$variation = self::with(['images' => function ($query) {
$query->active()->ordered();
}])->find($variationId);
if (!$variation) {
return collect();
}
return $variation->images;
}
public static function getImagesDataById($variationId)
{
$images = self::getImagesByVariationId($variationId);
return $images->map(function ($image) {
return [
'id' => $image->id,
'url' => $image->url,
'sort_order' => $image->sort_order,
'variation_id' => $image->variation_id
];
});
}
}

74
app/Models/Review.php Normal file
View File

@@ -0,0 +1,74 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Review extends Model
{
protected $fillable = [
'user_id',
'product_id',
'rating',
'title',
'comment',
'advantages',
'disadvantages',
'helpful_count',
'unhelpful_count',
'is_approved'
];
protected $casts = [
'is_approved' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
}
public function getDateAttribute(): string
{
return $this->created_at->format('d.m.Y');
}
public function getIsFreshAttribute(): bool
{
return $this->created_at->gt(now()->subWeek());
}
public function scopeApproved($query)
{
return $query->where('is_approved', true);
}
public function scopeByRating($query, $rating)
{
return $query->where('rating', $rating);
}
public function scopeHelpful($query)
{
return $query->orderBy('helpful_count', 'desc');
}
public function scopeLatest($query)
{
return $query->orderBy('created_at', 'desc');
}
public function scopeUserProduct($query, $userId, $productId)
{
return $query->where('user_id', $userId)
->where('product_id', $productId);
}
}

32
app/Models/Role.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Role extends Model
{
public $timestamps = false;
protected $fillable = [
'name',
'slug',
'description',
];
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class)->withTimestamps();
}
public function permissions(): BelongsToMany
{
return $this->belongsToMany(Permission::class)->withTimestamps();
}
public function hasPermission(string $permissionSlug): bool
{
return $this->permissions()->where('slug', $permissionSlug)->exists();
}
}

110
app/Models/User.php Normal file
View File

@@ -0,0 +1,110 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
'phone',
'role_id',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
public function role(): BelongsTo
{
return $this->belongsTo(Role::class);
}
public function permissions(): BelongsToMany
{
return $this->belongsToMany(Permission::class, 'role_user', 'user_id', 'role_id')
->join('permission_role', 'role_user.role_id', '=', 'permission_role.role_id')
->join('permissions', 'permission_role.permission_id', '=', 'permissions.id')
->select('permissions.*')
->distinct()
->withTimestamps();
}
public function hasRole(string $roleSlug): bool
{
return $this->role()->where('slug', $roleSlug)->exists();
}
public function hasPermission(string $permissionSlug): bool
{
if ($this->hasRole('super_admin')) {
return true;
}
$role = $this->role;
return $role && $role->hasPermission($permissionSlug);
}
public function hasAnyPermission(array $permissionSlugs): bool
{
if ($this->is_super_admin) {
return true;
}
foreach ($permissionSlugs as $permissionSlug) {
if ($this->hasPermission($permissionSlug)) {
return true;
}
}
return false;
}
public function hasAllPermissions(array $permissionSlugs): bool
{
if ($this->is_super_admin) {
return true;
}
foreach ($permissionSlugs as $permissionSlug) {
if (!$this->hasPermission($permissionSlug)) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class VariationAttribute extends Model
{
protected $fillable = ['variation_id', 'key', 'value'];
public function variation(): BelongsTo
{
return $this->belongsTo(ProductVariation::class, 'variation_id');
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class VariationImage extends Model
{
public $timestamps = false;
protected $fillable = [
'variation_id',
'path',
'sort_order',
'is_active'
];
protected $casts = [
'sort_order' => 'integer',
'is_active' => 'boolean'
];
public function variation(): BelongsTo
{
return $this->belongsTo(ProductVariation::class, 'variation_id');
}
public function getUrlAttribute(): string
{
if ($this->path && file_exists(public_path($this->path))) {
return asset($this->path);
}
return asset('assets/images/products/default.svg');
}
public function scopeOrdered($query)
{
return $query->orderBy('sort_order', 'asc');
}
public function scopeActive($query)
{
return $query->where('is_active', true);
}
}