¨4.0.1¨
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
namespace Modules\Product\Admin;
|
||||
|
||||
use Modules\Admin\Ui\AdminTable;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Modules\Product\Entities\Product;
|
||||
|
||||
class ProductTable extends AdminTable
|
||||
@@ -12,24 +13,25 @@ class ProductTable extends AdminTable
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $rawColumns = ['price'];
|
||||
protected array $rawColumns = ['price'];
|
||||
|
||||
|
||||
/**
|
||||
* Make table response for the resource.
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function make()
|
||||
{
|
||||
return $this->newTable()
|
||||
->editColumn('thumbnail', function ($product) {
|
||||
return view('admin::partials.table.image', [
|
||||
'file' => $product->base_image,
|
||||
'file' => $product->variant?->base_image ?? $product->base_image,
|
||||
]);
|
||||
})
|
||||
->editColumn('price', function (Product $product) {
|
||||
return product_price_formatted($product, function ($price, $specialPrice) use ($product) {
|
||||
if ($product->hasSpecialPrice()) {
|
||||
return product_price_formatted($product->variant ?? $product, function ($price, $specialPrice) use ($product) {
|
||||
if ($product->variant ? $product->variant->hasSpecialPrice() : $product->hasSpecialPrice()) {
|
||||
return "<span class='m-r-5'>{$specialPrice}</span>
|
||||
<del class='text-red'>{$price}</del>";
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateProductsTable extends Migration
|
||||
class
|
||||
CreateProductsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
@@ -35,6 +36,7 @@ class CreateProductsTable extends Migration
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
@@ -28,6 +29,7 @@ class CreateProductTranslationsTable extends Migration
|
||||
DB::statement('ALTER TABLE product_translations ADD FULLTEXT(name)');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
|
||||
@@ -23,6 +23,7 @@ class CreateRelatedProductsTable extends Migration
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
|
||||
@@ -23,6 +23,7 @@ class CreateUpSellProductsTable extends Migration
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
|
||||
@@ -23,6 +23,7 @@ class CreateCrossSellProductsTable extends Migration
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
|
||||
@@ -23,6 +23,7 @@ class CreateProductCategoriesTable extends Migration
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
|
||||
@@ -22,6 +22,7 @@ class CreateSearchTermsTable extends Migration
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
|
||||
@@ -18,6 +18,7 @@ class AddSpecialPriceTypeColumnToProductsTable extends Migration
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
|
||||
@@ -20,6 +20,7 @@ class AddBrandIdColumnToProductsTable extends Migration
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
|
||||
@@ -23,6 +23,7 @@ class CreateProductTagsTable extends Migration
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
|
||||
@@ -14,10 +14,11 @@ class AddDownloadsColumnsToProductsTable extends Migration
|
||||
public function up()
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->boolean('virtual')->default(false)->before('is_active');
|
||||
$table->boolean('is_virtual')->default(false)->before('is_active');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
@@ -26,7 +27,7 @@ class AddDownloadsColumnsToProductsTable extends Migration
|
||||
public function down()
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->dropColumn('virtual');
|
||||
$table->dropColumn('is_virtual');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
return new class extends Migration {
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
@@ -14,10 +13,13 @@ return new class extends Migration
|
||||
public function up()
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->renameColumn('virtual', 'is_virtual');
|
||||
if (Schema::hasColumn('products', 'virtual')) {
|
||||
$table->renameColumn('virtual', 'is_virtual');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
@@ -26,7 +28,9 @@ return new class extends Migration
|
||||
public function down()
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->renameColumn('is_virtual', 'virtual');
|
||||
if (Schema::hasColumn('products', 'is_virtual')) {
|
||||
$table->renameColumn('is_virtual', 'virtual');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateProductVariationsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('product_variations', function (Blueprint $table) {
|
||||
$table->integer('product_id')->unsigned();
|
||||
$table->integer('variation_id')->unsigned();
|
||||
|
||||
$table->primary(['product_id', 'variation_id']);
|
||||
$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
|
||||
$table->foreign('variation_id')->references('id')->on('variations')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('product_variations');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateProductVariantsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('product_variants', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('uid');
|
||||
$table->text('uids');
|
||||
$table->integer('product_id')->unsigned();
|
||||
$table->string('name');
|
||||
$table->decimal('price', 18, 4)->unsigned()->nullable();
|
||||
$table->decimal('special_price', 18, 4)->unsigned()->nullable();
|
||||
$table->string('special_price_type')->nullable();
|
||||
$table->date('special_price_start')->nullable();
|
||||
$table->date('special_price_end')->nullable();
|
||||
$table->decimal('selling_price', 18, 4)->unsigned()->nullable();
|
||||
$table->string('sku')->nullable();
|
||||
$table->boolean('manage_stock')->nullable();
|
||||
$table->integer('qty')->nullable();
|
||||
$table->boolean('in_stock')->nullable();
|
||||
$table->boolean('is_default')->nullable();
|
||||
$table->boolean('is_active')->nullable();
|
||||
$table->integer('position')->unsigned()->nullable();
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('product_variants');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddTimeStampColumnsToRelatedProductsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('related_products', function (Blueprint $table) {
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('related_products', function (Blueprint $table) {
|
||||
$table->dropTimestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddTimeStampColumnsToUpSellProductsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('up_sell_products', function (Blueprint $table) {
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('up_sell_products', function (Blueprint $table) {
|
||||
$table->dropTimestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddTimeStampColumnsToCrossSellProductsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('cross_sell_products', function (Blueprint $table) {
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('cross_sell_products', function (Blueprint $table) {
|
||||
$table->dropTimestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
|
||||
class MakePriceColumnInTheProductsTableNullable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('products', 'price')) {
|
||||
$table->decimal('price', 18, 4)->nullable()->change();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('products', 'price')) {
|
||||
$table->decimal('price', 18, 4)->nullable(false)->change();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
;
|
||||
94
Modules/Product/Entities/Concerns/EloquentRelations.php
Normal file
94
Modules/Product/Entities/Concerns/EloquentRelations.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Entities\Concerns;
|
||||
|
||||
use Modules\Tag\Entities\Tag;
|
||||
use Modules\Brand\Entities\Brand;
|
||||
use Modules\Tax\Entities\TaxClass;
|
||||
use Modules\Option\Entities\Option;
|
||||
use Modules\Review\Entities\Review;
|
||||
use Modules\Category\Entities\Category;
|
||||
use Modules\Variation\Entities\Variation;
|
||||
use Modules\Product\Entities\ProductVariant;
|
||||
use Modules\Attribute\Entities\ProductAttribute;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
trait EloquentRelations
|
||||
{
|
||||
public function brand(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Brand::class)->withDefault();
|
||||
}
|
||||
|
||||
|
||||
public function tags(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Tag::class, 'product_tags');
|
||||
}
|
||||
|
||||
|
||||
public function categories(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Category::class, 'product_categories');
|
||||
}
|
||||
|
||||
|
||||
public function taxClass(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(TaxClass::class)->withDefault();
|
||||
}
|
||||
|
||||
|
||||
public function reviews(): HasMany
|
||||
{
|
||||
return $this->hasMany(Review::class);
|
||||
}
|
||||
|
||||
|
||||
public function attributes(): HasMany
|
||||
{
|
||||
return $this->hasMany(ProductAttribute::class);
|
||||
}
|
||||
|
||||
|
||||
public function variations(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Variation::class, 'product_variations')
|
||||
->orderBy('position')
|
||||
->withTrashed();
|
||||
}
|
||||
|
||||
|
||||
public function variants(): HasMany
|
||||
{
|
||||
return $this->hasMany(ProductVariant::class, 'product_id');
|
||||
}
|
||||
|
||||
|
||||
public function options(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Option::class, 'product_options')
|
||||
->orderBy('position')
|
||||
->withTrashed();
|
||||
}
|
||||
|
||||
|
||||
public function relatedProducts(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(static::class, 'related_products', 'product_id', 'related_product_id');
|
||||
}
|
||||
|
||||
|
||||
public function upSellProducts(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(static::class, 'up_sell_products', 'product_id', 'up_sell_product_id');
|
||||
}
|
||||
|
||||
|
||||
public function crossSellProducts(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(static::class, 'cross_sell_products', 'product_id', 'cross_sell_product_id');
|
||||
}
|
||||
}
|
||||
11
Modules/Product/Entities/Concerns/Filterable.php
Normal file
11
Modules/Product/Entities/Concerns/Filterable.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Entities\Concerns;
|
||||
|
||||
trait Filterable
|
||||
{
|
||||
public function filter($filter)
|
||||
{
|
||||
return $filter->apply($this);
|
||||
}
|
||||
}
|
||||
78
Modules/Product/Entities/Concerns/HasSpecialPrice.php
Normal file
78
Modules/Product/Entities/Concerns/HasSpecialPrice.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Entities\Concerns;
|
||||
|
||||
use Modules\Support\Money;
|
||||
|
||||
trait HasSpecialPrice
|
||||
{
|
||||
public function getSpecialPrice(): Money
|
||||
{
|
||||
$specialPrice = $this->attributes['special_price'];
|
||||
|
||||
if ($this->special_price_type === 'percent') {
|
||||
$discountedPrice = ($specialPrice / 100) * $this->attributes['price'];
|
||||
|
||||
$specialPrice = $this->attributes['price'] - $discountedPrice;
|
||||
}
|
||||
|
||||
if ($specialPrice < 0) {
|
||||
$specialPrice = 0;
|
||||
}
|
||||
|
||||
return Money::inDefaultCurrency($specialPrice);
|
||||
}
|
||||
|
||||
|
||||
public function hasSpecialPrice(): bool
|
||||
{
|
||||
if (is_null($this->special_price)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->hasSpecialPriceStartDate() && $this->hasSpecialPriceEndDate()) {
|
||||
return $this->specialPriceStartDateIsValid() && $this->specialPriceEndDateIsValid();
|
||||
}
|
||||
|
||||
if ($this->hasSpecialPriceStartDate()) {
|
||||
return $this->specialPriceStartDateIsValid();
|
||||
}
|
||||
|
||||
if ($this->hasSpecialPriceEndDate()) {
|
||||
return $this->specialPriceEndDateIsValid();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public function hasPercentageSpecialPrice(): bool
|
||||
{
|
||||
return $this->hasSpecialPrice() && $this->special_price_type === 'percent';
|
||||
}
|
||||
|
||||
|
||||
private function hasSpecialPriceStartDate(): bool
|
||||
{
|
||||
return !is_null($this->special_price_start);
|
||||
}
|
||||
|
||||
|
||||
private function hasSpecialPriceEndDate(): bool
|
||||
{
|
||||
return !is_null($this->special_price_end);
|
||||
}
|
||||
|
||||
|
||||
private function specialPriceStartDateIsValid(): bool
|
||||
{
|
||||
return today() >= $this->special_price_start;
|
||||
}
|
||||
|
||||
|
||||
private function specialPriceEndDateIsValid(): bool
|
||||
{
|
||||
return today() <= $this->special_price_end;
|
||||
}
|
||||
|
||||
}
|
||||
59
Modules/Product/Entities/Concerns/HasStock.php
Normal file
59
Modules/Product/Entities/Concerns/HasStock.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Entities\Concerns;
|
||||
|
||||
use Modules\FlashSale\Entities\FlashSale;
|
||||
|
||||
trait HasStock
|
||||
{
|
||||
public function isOutOfStock(): bool
|
||||
{
|
||||
return !$this->isInStock();
|
||||
}
|
||||
|
||||
|
||||
public function isInStock()
|
||||
{
|
||||
if (FlashSale::contains($this)) {
|
||||
return FlashSale::remainingQty($this) > 0;
|
||||
}
|
||||
if ($this->hasAnyVariants()) {
|
||||
$productWithStock = $this->variants()
|
||||
->where(function ($query) {
|
||||
$query->where(
|
||||
[
|
||||
['manage_stock', true],
|
||||
['qty', '>', 0],
|
||||
]);
|
||||
|
||||
$query->orWhere('manage_stock', false);
|
||||
})
|
||||
->where('in_stock', true)
|
||||
->first();
|
||||
|
||||
return (bool)$productWithStock;
|
||||
} else {
|
||||
if ($this->manage_stock && $this->qty === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->in_stock;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function markAsInStock(): void
|
||||
{
|
||||
$this->withoutEvents(function () {
|
||||
$this->update(['in_stock' => true]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public function markAsOutOfStock(): void
|
||||
{
|
||||
$this->withoutEvents(function () {
|
||||
$this->update(['in_stock' => false]);
|
||||
});
|
||||
}
|
||||
}
|
||||
47
Modules/Product/Entities/Concerns/IsNew.php
Normal file
47
Modules/Product/Entities/Concerns/IsNew.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Entities\Concerns;
|
||||
|
||||
trait IsNew
|
||||
{
|
||||
public function isNew(): bool
|
||||
{
|
||||
if ($this->hasNewFromDate() && $this->hasNewToDate()) {
|
||||
return $this->newFromDateIsValid() && $this->newToDateIsValid();
|
||||
}
|
||||
|
||||
if ($this->hasNewFromDate()) {
|
||||
return $this->newFromDateIsValid();
|
||||
}
|
||||
|
||||
if ($this->hasNewToDate()) {
|
||||
return $this->newToDateIsValid();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private function hasNewFromDate(): bool
|
||||
{
|
||||
return !is_null($this->new_from);
|
||||
}
|
||||
|
||||
|
||||
private function hasNewToDate(): bool
|
||||
{
|
||||
return !is_null($this->new_to);
|
||||
}
|
||||
|
||||
|
||||
private function newFromDateIsValid(): bool
|
||||
{
|
||||
return today() >= $this->new_from;
|
||||
}
|
||||
|
||||
|
||||
private function newToDateIsValid(): bool
|
||||
{
|
||||
return today() <= $this->new_to;
|
||||
}
|
||||
}
|
||||
190
Modules/Product/Entities/Concerns/ModelAccessors.php
Normal file
190
Modules/Product/Entities/Concerns/ModelAccessors.php
Normal file
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Entities\Concerns;
|
||||
|
||||
use Modules\Support\Money;
|
||||
use Modules\Media\Entities\File;
|
||||
use Modules\FlashSale\Entities\FlashSale;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Modules\FlashSale\Entities\FlashSaleProduct;
|
||||
|
||||
trait ModelAccessors
|
||||
{
|
||||
public function getVariantAttribute()
|
||||
{
|
||||
if (request()->query('variant')) {
|
||||
return $this->variants()->where('uid', request()->query('variant'))->first();
|
||||
}
|
||||
|
||||
return $this->variants()->default()->first();
|
||||
}
|
||||
|
||||
|
||||
public function getIsInFlashSaleAttribute()
|
||||
{
|
||||
return FlashSale::contains($this);
|
||||
}
|
||||
|
||||
|
||||
public function getFlashSaleEndDateAttribute()
|
||||
{
|
||||
if (FlashSale::contains($this)) {
|
||||
return FlashSaleProduct::where('product_id', $this->id)->first()?->end_date;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getPriceAttribute($price): Money
|
||||
{
|
||||
return Money::inDefaultCurrency($price);
|
||||
}
|
||||
|
||||
|
||||
public function getFormattedPriceAttribute(): string
|
||||
{
|
||||
return product_price_formatted($this);
|
||||
}
|
||||
|
||||
|
||||
public function getFormattedPriceRangeAttribute(): ?string
|
||||
{
|
||||
if ($this->variants()->exists()) {
|
||||
$minPrice = $this->variants()->min('price');
|
||||
$maxPrice = $this->variants()->max('price');
|
||||
|
||||
if ($minPrice !== $maxPrice) {
|
||||
$formattedMinPriceInCurrentCurrency = Money::inDefaultCurrency($minPrice)->convertToCurrentCurrency()->format();
|
||||
$formattedMaxPriceInCurrentCurrency = Money::inDefaultCurrency($maxPrice)->convertToCurrentCurrency()->format();
|
||||
|
||||
return "$formattedMinPriceInCurrentCurrency - $formattedMaxPriceInCurrentCurrency";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public function getSpecialPriceAttribute($specialPrice)
|
||||
{
|
||||
if (!is_null($specialPrice)) {
|
||||
return Money::inDefaultCurrency($specialPrice);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getHasPercentageSpecialPriceAttribute(): bool
|
||||
{
|
||||
return $this->hasPercentageSpecialPrice();
|
||||
}
|
||||
|
||||
|
||||
public function getSpecialPricePercentAttribute()
|
||||
{
|
||||
if ($this->hasPercentageSpecialPrice()) {
|
||||
return round($this->special_price->amount(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getSellingPriceAttribute($sellingPrice): Money
|
||||
{
|
||||
if (FlashSale::contains($this)) {
|
||||
$sellingPrice = FlashSale::pivot($this)->price->amount();
|
||||
}
|
||||
|
||||
return Money::inDefaultCurrency($sellingPrice);
|
||||
}
|
||||
|
||||
|
||||
public function getTotalAttribute($total): Money
|
||||
{
|
||||
return Money::inDefaultCurrency($total);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the product's base image.
|
||||
*
|
||||
* @return File
|
||||
*/
|
||||
public function getBaseImageAttribute(): File
|
||||
{
|
||||
return $this->files->where('pivot.zone', 'base_image')->first() ?: new File();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get product's additional images.
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getAdditionalImagesAttribute(): Collection
|
||||
{
|
||||
return $this->files->where('pivot.zone', 'additional_images')
|
||||
->sortBy('pivot.id');
|
||||
}
|
||||
|
||||
|
||||
public function getMediaAttribute()
|
||||
{
|
||||
return $this->filterFiles(['base_image', 'additional_images'])->get();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get product's downloadable files.
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getDownloadsAttribute()
|
||||
{
|
||||
return $this->files
|
||||
->where('pivot.zone', 'downloads')
|
||||
->sortBy('pivot.id')
|
||||
->flatten();
|
||||
}
|
||||
|
||||
|
||||
public function getDoesManageStockAttribute(): bool
|
||||
{
|
||||
return (bool)$this->manage_stock;
|
||||
}
|
||||
|
||||
|
||||
public function getQtyAttribute($qty)
|
||||
{
|
||||
return $qty;
|
||||
}
|
||||
|
||||
|
||||
public function getIsInStockAttribute(): bool
|
||||
{
|
||||
return (bool)$this->isInStock();
|
||||
}
|
||||
|
||||
|
||||
public function getIsOutOfStockAttribute(): bool
|
||||
{
|
||||
return $this->isOutOfStock();
|
||||
}
|
||||
|
||||
|
||||
public function getIsNewAttribute(): bool
|
||||
{
|
||||
return $this->isNew();
|
||||
}
|
||||
|
||||
|
||||
public function getAttributeSetsAttribute()
|
||||
{
|
||||
return $this->getAttribute('attributes')->groupBy('attributeSet');
|
||||
}
|
||||
|
||||
|
||||
public function getRatingPercentAttribute()
|
||||
{
|
||||
if ($this->relationLoaded('reviews')) {
|
||||
return ($this->reviews->avg->rating / 5) * 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
Modules/Product/Entities/Concerns/Predicates.php
Normal file
40
Modules/Product/Entities/Concerns/Predicates.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Entities\Concerns;
|
||||
|
||||
trait Predicates
|
||||
{
|
||||
/**
|
||||
* Is this Product purchased by the current user?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function purchasedByUser(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public function hasAnyVariation()
|
||||
{
|
||||
return $this->getAttribute('variations')->isNotEmpty();
|
||||
}
|
||||
|
||||
|
||||
public function hasAnyVariants(): bool
|
||||
{
|
||||
return $this->getAttribute('variants')->isNotEmpty();
|
||||
}
|
||||
|
||||
|
||||
public function hasAnyOption(): bool
|
||||
{
|
||||
return $this->getAttribute('options')->isNotEmpty();
|
||||
}
|
||||
|
||||
|
||||
public function hasAnyAttribute(): bool
|
||||
{
|
||||
return $this->getAttribute('attributes')->isNotEmpty();
|
||||
}
|
||||
}
|
||||
78
Modules/Product/Entities/Concerns/QueryScopes.php
Normal file
78
Modules/Product/Entities/Concerns/QueryScopes.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Entities\Concerns;
|
||||
|
||||
trait QueryScopes
|
||||
{
|
||||
public function scopeForCard($query): void
|
||||
{
|
||||
$query
|
||||
->withName()
|
||||
->withBaseImage()
|
||||
->withPrice()
|
||||
->withCount('options')
|
||||
->with('reviews')
|
||||
->withStock()
|
||||
->withNew()
|
||||
->addSelect(
|
||||
[
|
||||
'products.id',
|
||||
'products.slug',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function scopeWithName($query): void
|
||||
{
|
||||
$query->with('translations:id,product_id,locale,name');
|
||||
}
|
||||
|
||||
|
||||
public function scopeWithStock($query): void
|
||||
{
|
||||
$query->addSelect(
|
||||
[
|
||||
'products.in_stock',
|
||||
'products.manage_stock',
|
||||
'products.qty',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function scopeWithNew($query): void
|
||||
{
|
||||
$query->addSelect(
|
||||
[
|
||||
'products.new_from',
|
||||
'products.new_to',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function scopeWithPrice($query): void
|
||||
{
|
||||
$query->addSelect(
|
||||
[
|
||||
'products.price',
|
||||
'products.special_price',
|
||||
'products.special_price_type',
|
||||
'products.selling_price',
|
||||
'products.special_price_start',
|
||||
'products.special_price_end',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function scopeWithBaseImage($query): void
|
||||
{
|
||||
$query->with([
|
||||
'files' => function ($q) {
|
||||
$q->wherePivot('zone', 'base_image');
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -2,28 +2,41 @@
|
||||
|
||||
namespace Modules\Product\Entities;
|
||||
|
||||
use Modules\Support\Money;
|
||||
use Modules\Tag\Entities\Tag;
|
||||
use Modules\Media\Entities\File;
|
||||
use Modules\Brand\Entities\Brand;
|
||||
use Modules\Tax\Entities\TaxClass;
|
||||
use Modules\Option\Entities\Option;
|
||||
use Modules\Review\Entities\Review;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\Support\Eloquent\Model;
|
||||
use Modules\Media\Eloquent\HasMedia;
|
||||
use Modules\Meta\Eloquent\HasMetaData;
|
||||
use Modules\Support\Search\Searchable;
|
||||
use Modules\Category\Entities\Category;
|
||||
use Modules\Product\Admin\ProductTable;
|
||||
use Modules\Support\Eloquent\Sluggable;
|
||||
use Modules\FlashSale\Entities\FlashSale;
|
||||
use Modules\Support\Eloquent\Translatable;
|
||||
use Modules\Product\Entities\Concerns\IsNew;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Modules\Attribute\Entities\ProductAttribute;
|
||||
use Modules\Product\Entities\Concerns\HasStock;
|
||||
use Modules\Product\Entities\Concerns\Predicates;
|
||||
use Modules\Product\Entities\Concerns\Filterable;
|
||||
use Modules\Product\Entities\Concerns\QueryScopes;
|
||||
use Modules\Product\Entities\Concerns\ModelAccessors;
|
||||
use Modules\Product\Entities\Concerns\HasSpecialPrice;
|
||||
use Modules\Product\Entities\Concerns\EloquentRelations;
|
||||
|
||||
|
||||
class Product extends Model
|
||||
{
|
||||
use Translatable, Searchable, Sluggable, HasMedia, HasMetaData, SoftDeletes;
|
||||
use Translatable,
|
||||
Searchable,
|
||||
Filterable,
|
||||
Sluggable,
|
||||
HasMedia,
|
||||
HasMetaData,
|
||||
HasSpecialPrice,
|
||||
HasStock,
|
||||
SoftDeletes,
|
||||
IsNew,
|
||||
QueryScopes,
|
||||
ModelAccessors,
|
||||
Predicates,
|
||||
EloquentRelations;
|
||||
|
||||
/**
|
||||
* The relations to eager load on every query.
|
||||
@@ -37,7 +50,25 @@ class Product extends Model
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = ['brand_id', 'tax_class_id', 'slug', 'sku', 'price', 'special_price', 'special_price_type', 'special_price_start', 'special_price_end', 'selling_price', 'manage_stock', 'qty', 'in_stock', 'is_virtual', 'is_active', 'new_from', 'new_to'];
|
||||
protected $fillable = [
|
||||
'brand_id',
|
||||
'tax_class_id',
|
||||
'slug',
|
||||
'sku',
|
||||
'price',
|
||||
'special_price',
|
||||
'special_price_type',
|
||||
'special_price_start',
|
||||
'special_price_end',
|
||||
'selling_price',
|
||||
'manage_stock',
|
||||
'qty',
|
||||
'in_stock',
|
||||
'is_virtual',
|
||||
'is_active',
|
||||
'new_from',
|
||||
'new_to',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to native types.
|
||||
@@ -45,8 +76,7 @@ class Product extends Model
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'manage_stock' => 'boolean',
|
||||
'in_stock' => 'boolean',
|
||||
'is_virtual' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
@@ -55,478 +85,100 @@ class Product extends Model
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['special_price_start', 'special_price_end', 'new_from', 'new_to', 'start_date', 'end_date', 'deleted_at'];
|
||||
protected $dates = [
|
||||
'special_price_start',
|
||||
'special_price_end',
|
||||
'new_from',
|
||||
'new_to',
|
||||
'deleted_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* The accessors to append to the model's array form.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $appends = ['base_image', 'formatted_price', 'rating_percent', 'is_in_stock', 'is_out_of_stock', 'is_new', 'has_percentage_special_price', 'special_price_percent'];
|
||||
protected $appends = [
|
||||
'base_image',
|
||||
'additional_images',
|
||||
'media',
|
||||
'formatted_price',
|
||||
'formatted_price_range',
|
||||
'has_percentage_special_price',
|
||||
'special_price_percent',
|
||||
'rating_percent',
|
||||
'does_manage_stock',
|
||||
'is_in_stock',
|
||||
'is_out_of_stock',
|
||||
'is_new',
|
||||
'variant',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that are translatable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $translatedAttributes = ['name', 'description', 'short_description'];
|
||||
protected array $translatedAttributes = [
|
||||
'name',
|
||||
'description',
|
||||
'short_description',
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* The attribute that will be slugged.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $slugAttribute = 'name';
|
||||
protected string $slugAttribute = 'name';
|
||||
|
||||
|
||||
/**
|
||||
* Perform any actions required after the model boots.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected static function booted()
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::addActiveGlobalScope();
|
||||
|
||||
static::saved(function ($product) {
|
||||
if (!empty(request()->all())) {
|
||||
$product->saveRelations(request()->all());
|
||||
$attributes = request()->all();
|
||||
|
||||
if (!empty($attributes)) {
|
||||
$product->categories()->sync(array_get($attributes, 'categories', []));
|
||||
$product->tags()->sync(array_get($attributes, 'tags', []));
|
||||
$product->upSellProducts()->sync(array_get($attributes, 'up_sells', []));
|
||||
$product->crossSellProducts()->sync(array_get($attributes, 'cross_sells', []));
|
||||
$product->relatedProducts()->sync(array_get($attributes, 'related_products', []));
|
||||
}
|
||||
|
||||
$product->withoutEvents(function () use ($product) {
|
||||
$product->update(['selling_price' => $product->getSellingPrice()->amount()]);
|
||||
$product->update([
|
||||
'selling_price' => ($product->hasSpecialPrice() ? $product->getSpecialPrice() : $product->price)->amount(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
static::addActiveGlobalScope();
|
||||
}
|
||||
|
||||
public static function newArrivals($limit)
|
||||
{
|
||||
return static::forCard()
|
||||
->latest()
|
||||
->take($limit)
|
||||
->get();
|
||||
}
|
||||
|
||||
public static function list($ids = [])
|
||||
{
|
||||
return static::select('id')
|
||||
->withName()
|
||||
->whereIn('id', $ids)
|
||||
->when(!empty($ids), function ($query) use ($ids) {
|
||||
$idsString = collect($ids)
|
||||
->filter()
|
||||
->implode(',');
|
||||
|
||||
$query->orderByRaw("FIELD(id, {$idsString})");
|
||||
})
|
||||
->get()
|
||||
->mapWithKeys(function ($product) {
|
||||
return [$product->id => $product->name];
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeForCard($query)
|
||||
{
|
||||
$query
|
||||
->withName()
|
||||
->withBaseImage()
|
||||
->withPrice()
|
||||
->withCount('options')
|
||||
->with('reviews')
|
||||
->addSelect(['products.id', 'products.slug', 'products.in_stock', 'products.manage_stock', 'products.qty', 'products.new_from', 'products.new_to']);
|
||||
}
|
||||
|
||||
public function scopeWithPrice($query)
|
||||
{
|
||||
$query->addSelect(['products.price', 'products.special_price', 'products.special_price_type', 'products.selling_price', 'products.special_price_start', 'products.special_price_end']);
|
||||
}
|
||||
|
||||
public function scopeWithName($query)
|
||||
{
|
||||
$query->with('translations:id,product_id,locale,name');
|
||||
}
|
||||
|
||||
public function scopeWithBaseImage($query)
|
||||
{
|
||||
$query->with([
|
||||
'files' => function ($q) {
|
||||
$q->wherePivot('zone', 'base_image');
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
public function brand()
|
||||
{
|
||||
return $this->belongsTo(Brand::class)->withDefault();
|
||||
}
|
||||
|
||||
public function categories()
|
||||
{
|
||||
return $this->belongsToMany(Category::class, 'product_categories');
|
||||
}
|
||||
|
||||
public function taxClass()
|
||||
{
|
||||
return $this->belongsTo(TaxClass::class)->withDefault();
|
||||
}
|
||||
|
||||
public function tags()
|
||||
{
|
||||
return $this->belongsToMany(Tag::class, 'product_tags');
|
||||
}
|
||||
|
||||
public function reviews()
|
||||
{
|
||||
return $this->hasMany(Review::class);
|
||||
}
|
||||
|
||||
public function attributes()
|
||||
{
|
||||
return $this->hasMany(ProductAttribute::class);
|
||||
}
|
||||
|
||||
public function options()
|
||||
{
|
||||
return $this->belongsToMany(Option::class, 'product_options')
|
||||
->orderBy('position')
|
||||
->withTrashed();
|
||||
}
|
||||
|
||||
public function relatedProducts()
|
||||
{
|
||||
return $this->belongsToMany(static::class, 'related_products', 'product_id', 'related_product_id');
|
||||
}
|
||||
|
||||
public function upSellProducts()
|
||||
{
|
||||
return $this->belongsToMany(static::class, 'up_sell_products', 'product_id', 'up_sell_product_id');
|
||||
}
|
||||
|
||||
public function crossSellProducts()
|
||||
{
|
||||
return $this->belongsToMany(static::class, 'cross_sell_products', 'product_id', 'cross_sell_product_id');
|
||||
}
|
||||
|
||||
public function filter($filter)
|
||||
{
|
||||
return $filter->apply($this);
|
||||
}
|
||||
|
||||
public function getPriceAttribute($price)
|
||||
{
|
||||
return Money::inDefaultCurrency($price);
|
||||
}
|
||||
|
||||
public function getSpecialPriceAttribute($specialPrice)
|
||||
{
|
||||
if (!is_null($specialPrice)) {
|
||||
return Money::inDefaultCurrency($specialPrice);
|
||||
}
|
||||
}
|
||||
|
||||
public function getSellingPriceAttribute($sellingPrice)
|
||||
{
|
||||
if (FlashSale::contains($this)) {
|
||||
$sellingPrice = FlashSale::pivot($this)->price->amount();
|
||||
}
|
||||
|
||||
return Money::inDefaultCurrency($sellingPrice);
|
||||
}
|
||||
|
||||
public function getTotalAttribute($total)
|
||||
{
|
||||
return Money::inDefaultCurrency($total);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the product's base image.
|
||||
*
|
||||
* @return \Modules\Media\Entities\File
|
||||
*/
|
||||
public function getBaseImageAttribute()
|
||||
{
|
||||
return $this->files->where('pivot.zone', 'base_image')->first() ?: new File();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product's additional images.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public function getAdditionalImagesAttribute()
|
||||
{
|
||||
return $this->files->where('pivot.zone', 'additional_images')->sortBy('pivot.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product's downloadable files.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public function getDownloadsAttribute()
|
||||
{
|
||||
return $this->files
|
||||
->where('pivot.zone', 'downloads')
|
||||
->sortBy('pivot.id')
|
||||
->flatten();
|
||||
}
|
||||
|
||||
public function getFormattedPriceAttribute()
|
||||
{
|
||||
return product_price_formatted($this);
|
||||
}
|
||||
|
||||
public function getRatingPercentAttribute()
|
||||
{
|
||||
if ($this->relationLoaded('reviews')) {
|
||||
return $this->ratingPercent();
|
||||
}
|
||||
}
|
||||
|
||||
public function getIsInStockAttribute()
|
||||
{
|
||||
return $this->isInStock();
|
||||
}
|
||||
|
||||
public function getIsOutOfStockAttribute()
|
||||
{
|
||||
return $this->isOutOfStock();
|
||||
}
|
||||
|
||||
public function getIsNewAttribute()
|
||||
{
|
||||
return $this->isNew();
|
||||
}
|
||||
|
||||
public function getHasPercentageSpecialPriceAttribute()
|
||||
{
|
||||
return $this->hasPercentageSpecialPrice();
|
||||
}
|
||||
|
||||
public function getSpecialPricePercentAttribute()
|
||||
{
|
||||
return $this->getSpecialPricePercent();
|
||||
}
|
||||
|
||||
public function getAttributeSetsAttribute()
|
||||
{
|
||||
return $this->getAttribute('attributes')->groupBy('attributeSet');
|
||||
}
|
||||
|
||||
public function url()
|
||||
{
|
||||
return route('products.show', ['slug' => $this->slug]);
|
||||
}
|
||||
|
||||
public function isInStock()
|
||||
{
|
||||
if (FlashSale::contains($this)) {
|
||||
return FlashSale::remainingQty($this) > 0;
|
||||
}
|
||||
|
||||
if ($this->manage_stock && $this->qty === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->in_stock;
|
||||
}
|
||||
|
||||
public function isOutOfStock()
|
||||
{
|
||||
return !$this->isInStock();
|
||||
}
|
||||
|
||||
public function markAsInStock()
|
||||
{
|
||||
$this->withoutEvents(function () {
|
||||
$this->update(['in_stock' => true]);
|
||||
});
|
||||
}
|
||||
|
||||
public function markAsOutOfStock()
|
||||
{
|
||||
$this->withoutEvents(function () {
|
||||
$this->update(['in_stock' => false]);
|
||||
});
|
||||
}
|
||||
|
||||
public function hasAnyAttribute()
|
||||
{
|
||||
return $this->getAttribute('attributes')->isNotEmpty();
|
||||
}
|
||||
|
||||
public function hasAnyOption()
|
||||
{
|
||||
return $this->options->isNotEmpty();
|
||||
}
|
||||
|
||||
public function getSellingPrice()
|
||||
{
|
||||
if ($this->hasSpecialPrice()) {
|
||||
return $this->getSpecialPrice();
|
||||
}
|
||||
|
||||
return $this->price;
|
||||
}
|
||||
|
||||
public function getSpecialPrice()
|
||||
{
|
||||
$specialPrice = $this->attributes['special_price'];
|
||||
|
||||
if ($this->special_price_type === 'percent') {
|
||||
$discountedPrice = ($specialPrice / 100) * $this->attributes['price'];
|
||||
|
||||
$specialPrice = $this->attributes['price'] - $discountedPrice;
|
||||
}
|
||||
|
||||
if ($specialPrice < 0) {
|
||||
$specialPrice = 0;
|
||||
}
|
||||
|
||||
return Money::inDefaultCurrency($specialPrice);
|
||||
}
|
||||
|
||||
public function hasPercentageSpecialPrice()
|
||||
{
|
||||
return $this->hasSpecialPrice() && $this->special_price_type === 'percent';
|
||||
}
|
||||
|
||||
public function getSpecialPricePercent()
|
||||
{
|
||||
if ($this->hasPercentageSpecialPrice()) {
|
||||
return round($this->special_price->amount(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
public function hasSpecialPrice()
|
||||
{
|
||||
if (is_null($this->special_price)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->hasSpecialPriceStartDate() && $this->hasSpecialPriceEndDate()) {
|
||||
return $this->specialPriceStartDateIsValid() && $this->specialPriceEndDateIsValid();
|
||||
}
|
||||
|
||||
if ($this->hasSpecialPriceStartDate()) {
|
||||
return $this->specialPriceStartDateIsValid();
|
||||
}
|
||||
|
||||
if ($this->hasSpecialPriceEndDate()) {
|
||||
return $this->specialPriceEndDateIsValid();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function hasSpecialPriceStartDate()
|
||||
{
|
||||
return !is_null($this->special_price_start);
|
||||
}
|
||||
|
||||
private function hasSpecialPriceEndDate()
|
||||
{
|
||||
return !is_null($this->special_price_end);
|
||||
}
|
||||
|
||||
private function specialPriceStartDateIsValid()
|
||||
{
|
||||
return today() >= $this->special_price_start;
|
||||
}
|
||||
|
||||
private function specialPriceEndDateIsValid()
|
||||
{
|
||||
return today() <= $this->special_price_end;
|
||||
}
|
||||
|
||||
public function ratingPercent()
|
||||
{
|
||||
return ($this->reviews->avg->rating / 5) * 100;
|
||||
}
|
||||
|
||||
public function isNew()
|
||||
{
|
||||
if ($this->hasNewFromDate() && $this->hasNewToDate()) {
|
||||
return $this->newFromDateIsValid() && $this->newToDateIsValid();
|
||||
}
|
||||
|
||||
if ($this->hasNewFromDate()) {
|
||||
return $this->newFromDateIsValid();
|
||||
}
|
||||
|
||||
if ($this->hasNewToDate()) {
|
||||
return $this->newToDateIsValid();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function hasNewFromDate()
|
||||
{
|
||||
return !is_null($this->new_from);
|
||||
}
|
||||
|
||||
private function hasNewToDate()
|
||||
{
|
||||
return !is_null($this->new_to);
|
||||
}
|
||||
|
||||
private function newFromDateIsValid()
|
||||
{
|
||||
return today() >= $this->new_from;
|
||||
}
|
||||
|
||||
private function newToDateIsValid()
|
||||
{
|
||||
return today() <= $this->new_to;
|
||||
}
|
||||
|
||||
public function relatedProductList()
|
||||
{
|
||||
return $this->relatedProducts()
|
||||
->withoutGlobalScope('active')
|
||||
->pluck('related_product_id');
|
||||
}
|
||||
|
||||
public function upSellProductList()
|
||||
{
|
||||
return $this->upSellProducts()
|
||||
->withoutGlobalScope('active')
|
||||
->pluck('up_sell_product_id');
|
||||
}
|
||||
|
||||
public function crossSellProductList()
|
||||
{
|
||||
return $this->crossSellProducts()
|
||||
->withoutGlobalScope('active')
|
||||
->pluck('cross_sell_product_id');
|
||||
}
|
||||
|
||||
public static function findBySlug($slug)
|
||||
{
|
||||
return self::with(['categories', 'tags', 'attributes.attribute.attributeSet', 'options', 'files', 'relatedProducts', 'upSellProducts'])
|
||||
->where('slug', $slug)
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
public function clean()
|
||||
{
|
||||
return array_except($this->toArray(), ['description', 'short_description', 'translations', 'categories', 'files', 'is_active', 'in_stock', 'brand_id', 'tax_class', 'tax_class_id', 'viewed', 'created_at', 'updated_at', 'deleted_at']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get table data for the resource
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @param Request $request
|
||||
*
|
||||
* @return ProductTable
|
||||
*/
|
||||
public function table($request)
|
||||
public function table(Request $request): ProductTable
|
||||
{
|
||||
$query = $this->newQuery()
|
||||
->withoutGlobalScope('active')
|
||||
->withName()
|
||||
->withBaseImage()
|
||||
->withPrice()
|
||||
->addSelect(['id', 'is_active', 'created_at'])
|
||||
->addSelect(['id', 'is_active', 'created_at', 'updated_at'])
|
||||
->when($request->has('except'), function ($query) use ($request) {
|
||||
$query->whereNotIn('id', explode(',', $request->except));
|
||||
});
|
||||
@@ -534,29 +186,47 @@ class Product extends Model
|
||||
return new ProductTable($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save associated relations for the product.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @return void
|
||||
*/
|
||||
public function saveRelations($attributes = [])
|
||||
|
||||
public function clean(): array
|
||||
{
|
||||
$this->categories()->sync(array_get($attributes, 'categories', []));
|
||||
$this->tags()->sync(array_get($attributes, 'tags', []));
|
||||
$this->upSellProducts()->sync(array_get($attributes, 'up_sells', []));
|
||||
$this->crossSellProducts()->sync(array_get($attributes, 'cross_sells', []));
|
||||
$this->relatedProducts()->sync(array_get($attributes, 'related_products', []));
|
||||
$cleanExceptAttributes = [
|
||||
'description',
|
||||
'short_description',
|
||||
'translations',
|
||||
'categories',
|
||||
'files',
|
||||
'in_stock',
|
||||
'brand_id',
|
||||
'tax_class',
|
||||
'tax_class_id',
|
||||
'viewed',
|
||||
'is_active',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at',
|
||||
];
|
||||
|
||||
return array_except(
|
||||
$this->toArray(),
|
||||
$cleanExceptAttributes
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function url(): string
|
||||
{
|
||||
return route('products.show', ['slug' => $this->slug]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the indexable data array for the product.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toSearchableArray()
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
// MySQL Full-Text search handles indexing automatically.
|
||||
# MySQL Full-Text search handles indexing automatically.
|
||||
if (config('scout.driver') === 'mysql') {
|
||||
return [];
|
||||
}
|
||||
@@ -565,26 +235,48 @@ class Product extends Model
|
||||
->withoutGlobalScope('locale')
|
||||
->get(['name', 'description', 'short_description']);
|
||||
|
||||
return ['id' => $this->id, 'translations' => $translations];
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'translations' => $translations,
|
||||
];
|
||||
}
|
||||
|
||||
public function searchTable()
|
||||
|
||||
public function searchTable(): string
|
||||
{
|
||||
return 'product_translations';
|
||||
}
|
||||
|
||||
public function searchKey()
|
||||
|
||||
public function searchKey(): string
|
||||
{
|
||||
return 'product_id';
|
||||
}
|
||||
|
||||
public function searchColumns()
|
||||
|
||||
public function searchColumns(): array
|
||||
{
|
||||
return ['name'];
|
||||
}
|
||||
|
||||
public function searchExactColumn()
|
||||
|
||||
/**
|
||||
* Help HasMedia trait to extract media
|
||||
* for this model from the HTTP request.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function extractMediaFromRequest(): mixed
|
||||
{
|
||||
return 'name';
|
||||
$media = collect(request('media', []));
|
||||
|
||||
return [
|
||||
'base_image' => $media->first(),
|
||||
'additional_images' =>
|
||||
$media->except(
|
||||
$media->keys()->first()
|
||||
)->toArray(),
|
||||
'downloads' => request('downloads', []),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
372
Modules/Product/Entities/ProductVariant.php
Normal file
372
Modules/Product/Entities/ProductVariant.php
Normal file
@@ -0,0 +1,372 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Entities;
|
||||
|
||||
use Modules\Support\Money;
|
||||
use Modules\Media\Entities\File;
|
||||
use Modules\Support\Eloquent\Model;
|
||||
use Modules\Media\Eloquent\HasMedia;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class ProductVariant extends Model
|
||||
{
|
||||
use SoftDeletes, HasMedia;
|
||||
|
||||
/**
|
||||
* The relations to eager load on every query.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $with = [];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'uid',
|
||||
'uids',
|
||||
'name',
|
||||
'sku',
|
||||
'position',
|
||||
'price',
|
||||
'special_price',
|
||||
'special_price_type',
|
||||
'special_price_start',
|
||||
'special_price_end',
|
||||
'selling_price',
|
||||
'manage_stock',
|
||||
'qty',
|
||||
'in_stock',
|
||||
'is_active',
|
||||
'is_default',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to native types.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'is_default' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = [
|
||||
'special_price_start',
|
||||
'special_price_end',
|
||||
'deleted_at',
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
'base_image',
|
||||
'additional_images',
|
||||
'media',
|
||||
'formatted_price',
|
||||
'has_percentage_special_price',
|
||||
'special_price_percent',
|
||||
'does_manage_stock',
|
||||
'is_in_stock',
|
||||
'is_out_of_stock',
|
||||
];
|
||||
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::addActiveGlobalScope();
|
||||
|
||||
static::saved(function ($productVariant) {
|
||||
$productVariant->withoutEvents(function () use ($productVariant) {
|
||||
$productVariant->update([
|
||||
'selling_price' => ($productVariant->hasSpecialPrice() ? $productVariant->getSpecialPrice() : $productVariant->price)->amount(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public function url()
|
||||
{
|
||||
return route('products.show', ['slug' => $this->product->slug, 'variant' => $this->uid]);
|
||||
}
|
||||
|
||||
|
||||
public function scopeDefault($query)
|
||||
{
|
||||
return $query->where('is_default', 1);
|
||||
}
|
||||
|
||||
|
||||
public function scopeWithName($query)
|
||||
{
|
||||
$query->with('id,product_id,name');
|
||||
}
|
||||
|
||||
|
||||
public function scopeWithPrice($query)
|
||||
{
|
||||
$query->addSelect(['price', 'special_price', 'special_price_type', 'special_price_start', 'special_price_end']);
|
||||
}
|
||||
|
||||
|
||||
public function scopeWithBaseImage($query)
|
||||
{
|
||||
$query->with([
|
||||
'files' => function ($q) {
|
||||
$q->wherePivot('zone', '=', 'base_image')->orWherePivot('zone', '=', 'additional_images');
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
|
||||
|
||||
public function getPriceAttribute($price)
|
||||
{
|
||||
return Money::inDefaultCurrency($price);
|
||||
}
|
||||
|
||||
|
||||
public function getSpecialPriceAttribute($specialPrice)
|
||||
{
|
||||
if (!is_null($specialPrice)) {
|
||||
return Money::inDefaultCurrency($specialPrice);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getSellingPriceAttribute($sellingPrice)
|
||||
{
|
||||
return Money::inDefaultCurrency($sellingPrice);
|
||||
}
|
||||
|
||||
|
||||
public function getHasPercentageSpecialPriceAttribute()
|
||||
{
|
||||
return $this->hasPercentageSpecialPrice();
|
||||
}
|
||||
|
||||
|
||||
public function getSpecialPricePercentAttribute()
|
||||
{
|
||||
if ($this->hasPercentageSpecialPrice()) {
|
||||
return round($this->special_price->amount(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getTotalAttribute($total)
|
||||
{
|
||||
return Money::inDefaultCurrency($total);
|
||||
}
|
||||
|
||||
|
||||
public function getDoesManageStockAttribute(): bool
|
||||
{
|
||||
return (bool)$this->manage_stock;
|
||||
}
|
||||
|
||||
|
||||
public function getIsInStockAttribute(): bool
|
||||
{
|
||||
return (bool)$this->isInStock();
|
||||
}
|
||||
|
||||
|
||||
public function getIsOutOfStockAttribute(): bool
|
||||
{
|
||||
return $this->isOutOfStock();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the product's base image.
|
||||
*
|
||||
* @return File
|
||||
*/
|
||||
public function getBaseImageAttribute()
|
||||
{
|
||||
return $this->files->where('pivot.zone', 'base_image')->first() ?: new File();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the product's base image.
|
||||
*
|
||||
* @return File
|
||||
*/
|
||||
public function getAdditionalImagesAttribute()
|
||||
{
|
||||
return $this->files->where('pivot.zone', 'additional_images')
|
||||
->sortBy('pivot.id');
|
||||
}
|
||||
|
||||
|
||||
public function getMediaAttribute()
|
||||
{
|
||||
return $this->filterFiles(['base_image', 'additional_images'])->get();
|
||||
}
|
||||
|
||||
|
||||
public function getFormattedPriceAttribute()
|
||||
{
|
||||
return product_price_formatted($this);
|
||||
}
|
||||
|
||||
|
||||
public function hasSpecialPrice(): bool
|
||||
{
|
||||
if (is_null($this->special_price)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->hasSpecialPriceStartDate() && $this->hasSpecialPriceEndDate()) {
|
||||
return $this->specialPriceStartDateIsValid() && $this->specialPriceEndDateIsValid();
|
||||
}
|
||||
|
||||
if ($this->hasSpecialPriceStartDate()) {
|
||||
return $this->specialPriceStartDateIsValid();
|
||||
}
|
||||
|
||||
if ($this->hasSpecialPriceEndDate()) {
|
||||
return $this->specialPriceEndDateIsValid();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public function getSpecialPrice()
|
||||
{
|
||||
$specialPrice = $this->attributes['special_price'];
|
||||
|
||||
if ($this->special_price_type === 'percent') {
|
||||
$discountedPrice = ($specialPrice / 100) * $this->attributes['price'];
|
||||
|
||||
$specialPrice = $this->attributes['price'] - $discountedPrice;
|
||||
}
|
||||
|
||||
if ($specialPrice < 0) {
|
||||
$specialPrice = 0;
|
||||
}
|
||||
|
||||
return Money::inDefaultCurrency($specialPrice);
|
||||
}
|
||||
|
||||
|
||||
public function getSellingPrice()
|
||||
{
|
||||
if ($this->hasSpecialPrice()) {
|
||||
return $this->getSpecialPrice();
|
||||
}
|
||||
|
||||
return $this->price;
|
||||
}
|
||||
|
||||
|
||||
public function isInStock(): bool
|
||||
{
|
||||
if ($this->manage_stock && $this->qty === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool)$this->in_stock;
|
||||
}
|
||||
|
||||
|
||||
public function isOutOfStock(): bool
|
||||
{
|
||||
return !$this->isInStock();
|
||||
}
|
||||
|
||||
|
||||
public function markAsInStock()
|
||||
{
|
||||
$this->withoutEvents(function () {
|
||||
$this->update(['in_stock' => true]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public function markAsOutOfStock()
|
||||
{
|
||||
$this->withoutEvents(function () {
|
||||
$this->update(['in_stock' => false]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public function clean()
|
||||
{
|
||||
$cleanExceptAttributes = [
|
||||
'files',
|
||||
'is_active',
|
||||
'in_stock',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at',
|
||||
];
|
||||
|
||||
return array_except($this->toArray(), $cleanExceptAttributes);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Help HasMedia trait to extract media
|
||||
* for this model from the HTTP request.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function extractMediaFromRequest(): mixed
|
||||
{
|
||||
$media = collect(request('variants.' . $this->uid . '.media') ?? []);
|
||||
|
||||
return [
|
||||
'base_image' => $media->first(),
|
||||
'additional_images' =>
|
||||
$media->except(
|
||||
$media->keys()->first()
|
||||
)->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
private function hasPercentageSpecialPrice()
|
||||
{
|
||||
return $this->hasSpecialPrice() && $this->special_price_type === 'percent';
|
||||
}
|
||||
|
||||
|
||||
private function hasSpecialPriceStartDate(): bool
|
||||
{
|
||||
return !is_null($this->special_price_start);
|
||||
}
|
||||
|
||||
|
||||
private function hasSpecialPriceEndDate(): bool
|
||||
{
|
||||
return !is_null($this->special_price_end);
|
||||
}
|
||||
|
||||
|
||||
private function specialPriceStartDateIsValid(): bool
|
||||
{
|
||||
return today() >= $this->special_price_start;
|
||||
}
|
||||
|
||||
|
||||
private function specialPriceEndDateIsValid(): bool
|
||||
{
|
||||
return today() <= $this->special_price_end;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Modules\Product\Events;
|
||||
|
||||
use Modules\Product\Entities\Product;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ProductViewed
|
||||
@@ -11,10 +12,11 @@ class ProductViewed
|
||||
/**
|
||||
* The product entity.
|
||||
*
|
||||
* @var \Modules\Product\Entities\Product
|
||||
* @var Product
|
||||
*/
|
||||
public $product;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Modules\Product\Events;
|
||||
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class ShowingProductList
|
||||
{
|
||||
@@ -11,10 +12,11 @@ class ShowingProductList
|
||||
/**
|
||||
* Collection of product.
|
||||
*
|
||||
* @var \Illuminate\Database\Eloquent\Collection
|
||||
* @var Collection
|
||||
*/
|
||||
public $products;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
|
||||
@@ -9,18 +9,20 @@ class ProductFilter
|
||||
private $request;
|
||||
private $queryStringFilter;
|
||||
|
||||
|
||||
public function __construct(Request $request, QueryStringFilter $queryStringFilter)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->queryStringFilter = $queryStringFilter;
|
||||
}
|
||||
|
||||
|
||||
public function apply($query)
|
||||
{
|
||||
$query = $query->forCard();
|
||||
|
||||
foreach ($this->filters() as $name => $value) {
|
||||
if (! is_null($value)) {
|
||||
if (!is_null($value)) {
|
||||
$this->queryStringFilter->{$name}($query, $value);
|
||||
}
|
||||
}
|
||||
@@ -28,6 +30,7 @@ class ProductFilter
|
||||
return $query;
|
||||
}
|
||||
|
||||
|
||||
private function filters()
|
||||
{
|
||||
return array_filter($this->request->query(), function ($filter) {
|
||||
@@ -35,6 +38,7 @@ class ProductFilter
|
||||
}, ARRAY_FILTER_USE_KEY);
|
||||
}
|
||||
|
||||
|
||||
private function filterExists($filter)
|
||||
{
|
||||
return method_exists($this->queryStringFilter, $filter) &&
|
||||
|
||||
@@ -35,6 +35,7 @@ class QueryStringFilter
|
||||
'new_to',
|
||||
];
|
||||
|
||||
|
||||
public function sort($query, $sortType)
|
||||
{
|
||||
if ($this->sortTypeExists($sortType)) {
|
||||
@@ -42,25 +43,23 @@ class QueryStringFilter
|
||||
}
|
||||
}
|
||||
|
||||
private function sortTypeExists($sortType)
|
||||
{
|
||||
return in_array(strtolower($sortType), $this->sorts);
|
||||
}
|
||||
|
||||
public function relevance()
|
||||
{
|
||||
// Products are searched by relevant order by default.
|
||||
}
|
||||
|
||||
|
||||
public function alphabetic($query)
|
||||
{
|
||||
$query->join('product_translations', function (JoinClause $join) {
|
||||
$join->on('products.id', '=', 'product_translations.product_id');
|
||||
})
|
||||
->groupBy(array_merge($this->groupColumns, ['product_translations.name']))
|
||||
->orderBy('product_translations.name');
|
||||
->groupBy(array_merge($this->groupColumns, ['product_translations.name']))
|
||||
->orderBy('product_translations.name');
|
||||
}
|
||||
|
||||
|
||||
public function topRated($query)
|
||||
{
|
||||
$query->selectRaw('AVG(reviews.rating) as avg_rating')
|
||||
@@ -72,35 +71,50 @@ class QueryStringFilter
|
||||
->orderByDesc('avg_rating');
|
||||
}
|
||||
|
||||
|
||||
public function latest($query)
|
||||
{
|
||||
$query->latest();
|
||||
}
|
||||
|
||||
|
||||
public function priceLowToHigh($query)
|
||||
{
|
||||
$query->orderBy('selling_price');
|
||||
}
|
||||
|
||||
|
||||
public function priceHighToLow($query)
|
||||
{
|
||||
$query->orderByDesc('selling_price');
|
||||
}
|
||||
|
||||
|
||||
public function fromPrice($query, $price)
|
||||
{
|
||||
$query->where('selling_price', '>=', $this->convertPrice($price));
|
||||
$convertedPrice = $this->convertPrice($price);
|
||||
|
||||
$query->where(function ($productQuery) use ($convertedPrice) {
|
||||
$productQuery->where('selling_price', '>=', $convertedPrice);
|
||||
$productQuery->orWhereHas('variants', function ($variantQuery) use ($convertedPrice) {
|
||||
$variantQuery->where('selling_price', '>=', $convertedPrice);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public function toPrice($query, $price)
|
||||
{
|
||||
$query->where('selling_price', '<=', $this->convertPrice($price));
|
||||
$convertedPrice = $this->convertPrice($price);
|
||||
|
||||
$query->where(function ($productQuery) use ($convertedPrice) {
|
||||
$productQuery->where('selling_price', '<=', $convertedPrice);
|
||||
$productQuery->orWhereHas('variants', function ($variantQuery) use ($convertedPrice) {
|
||||
$variantQuery->where('selling_price', '<=', $convertedPrice);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private function convertPrice($price)
|
||||
{
|
||||
return Money::inCurrentCurrency($price)->convertToDefaultCurrency()->amount();
|
||||
}
|
||||
|
||||
public function brand($query, $slug)
|
||||
{
|
||||
@@ -109,6 +123,7 @@ class QueryStringFilter
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public function category($query, $slug)
|
||||
{
|
||||
$query->whereHas('categories', function ($categoryQuery) use ($slug) {
|
||||
@@ -116,6 +131,7 @@ class QueryStringFilter
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public function tag($query, $slug)
|
||||
{
|
||||
$query->whereHas('tags', function ($tagQuery) use ($slug) {
|
||||
@@ -123,6 +139,7 @@ class QueryStringFilter
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public function attribute($query, $attributeFilters)
|
||||
{
|
||||
foreach ($this->getAttributeIds($attributeFilters) as $index => $attributeId) {
|
||||
@@ -136,11 +153,25 @@ class QueryStringFilter
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function sortTypeExists($sortType)
|
||||
{
|
||||
return in_array(strtolower($sortType), $this->sorts);
|
||||
}
|
||||
|
||||
|
||||
private function convertPrice($price)
|
||||
{
|
||||
return Money::inCurrentCurrency($price)->convertToDefaultCurrency()->amount();
|
||||
}
|
||||
|
||||
|
||||
private function getAttributeIds($attributeFilters)
|
||||
{
|
||||
return Attribute::whereIn('slug', array_keys($attributeFilters))->pluck('id');
|
||||
}
|
||||
|
||||
|
||||
private function getAttributeValueIds($attributeFilters)
|
||||
{
|
||||
return once(function () use ($attributeFilters) {
|
||||
|
||||
@@ -2,9 +2,15 @@
|
||||
|
||||
namespace Modules\Product\Http\Controllers\Admin;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Modules\Product\Entities\Product;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Modules\Admin\Traits\HasCrudActions;
|
||||
use Modules\Product\Http\Requests\SaveProductRequest;
|
||||
use Modules\Product\Transformers\ProductEditResource;
|
||||
|
||||
class ProductController
|
||||
{
|
||||
@@ -15,26 +21,124 @@ class ProductController
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $model = Product::class;
|
||||
protected string $model = Product::class;
|
||||
|
||||
/**
|
||||
* Label of the resource.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $label = 'product::products.product';
|
||||
protected string $label = 'product::products.product';
|
||||
|
||||
/**
|
||||
* View path of the resource.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $viewPath = 'product::admin.products';
|
||||
protected string $viewPath = 'product::admin.products';
|
||||
|
||||
/**
|
||||
* Form requests for the resource.
|
||||
*
|
||||
* @var array|string
|
||||
*/
|
||||
protected $validation = SaveProductRequest::class;
|
||||
protected string|array $validation = SaveProductRequest::class;
|
||||
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @return Response|JsonResponse
|
||||
*/
|
||||
public function store()
|
||||
{
|
||||
$this->disableSearchSyncing();
|
||||
|
||||
$entity = $this->getModel()->create(
|
||||
$this->getRequest('store')->all()
|
||||
);
|
||||
|
||||
$this->searchable($entity);
|
||||
|
||||
$message = trans('admin::messages.resource_created', ['resource' => $this->getLabel()]);
|
||||
|
||||
if (request()->query('exit_flash')) {
|
||||
session()->flash('exit_flash', $message);
|
||||
}
|
||||
|
||||
if (request()->wantsJson()) {
|
||||
return response()->json(
|
||||
[
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'product_id' => $entity->id,
|
||||
], 200
|
||||
);
|
||||
}
|
||||
|
||||
return redirect()->route("{$this->getRoutePrefix()}.index")
|
||||
->withSuccess($message);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*
|
||||
* @param int $id
|
||||
*
|
||||
* @return Factory|View|Application
|
||||
*/
|
||||
public function edit($id): Factory|View|Application
|
||||
{
|
||||
$entity = $this->getEntity($id);
|
||||
$productEditResource = new ProductEditResource($entity);
|
||||
|
||||
return view("{$this->viewPath}.edit",
|
||||
[
|
||||
'product' => $entity,
|
||||
'product_resource' => $productEditResource->response()->content(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*
|
||||
* @param int $id
|
||||
*/
|
||||
public function update($id)
|
||||
{
|
||||
$entity = $this->getEntity($id);
|
||||
|
||||
$this->disableSearchSyncing();
|
||||
|
||||
$entity->update(
|
||||
$this->getRequest('update')->all()
|
||||
);
|
||||
|
||||
$entity->withoutEvents(function () use ($entity) {
|
||||
$entity->touch();
|
||||
});
|
||||
|
||||
$productEditResource = new ProductEditResource($entity);
|
||||
|
||||
$this->searchable($entity);
|
||||
|
||||
$message = trans('admin::messages.resource_updated', ['resource' => $this->getLabel()]);
|
||||
|
||||
if (request()->query('exit_flash')) {
|
||||
session()->flash('exit_flash', $message);
|
||||
}
|
||||
|
||||
if (request()->wantsJson()) {
|
||||
return response()->json(
|
||||
[
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'product_resource' => $productEditResource,
|
||||
], 200
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,17 @@
|
||||
|
||||
namespace Modules\Product\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Modules\Review\Entities\Review;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Modules\Product\Entities\Product;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Modules\Product\Events\ProductViewed;
|
||||
use Modules\Product\Filters\ProductFilter;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Modules\Product\Repositories\ProductRepository;
|
||||
use Modules\Product\Http\Middleware\SetProductSortOption;
|
||||
|
||||
class ProductController extends Controller
|
||||
@@ -23,12 +29,14 @@ class ProductController extends Controller
|
||||
$this->middleware(SetProductSortOption::class)->only('index');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @param \Modules\Product\Entities\Product $model
|
||||
* @param \Modules\Product\Filters\ProductFilter $productFilter
|
||||
* @return \Illuminate\Http\Response
|
||||
* @param Product $model
|
||||
* @param ProductFilter $productFilter
|
||||
*
|
||||
* @return JsonResponse|Application|Factory|View
|
||||
*/
|
||||
public function index(Product $model, ProductFilter $productFilter)
|
||||
{
|
||||
@@ -39,28 +47,37 @@ class ProductController extends Controller
|
||||
return view('public.products.index');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show the specified resource.
|
||||
*
|
||||
* @param string $slug
|
||||
* @return \Illuminate\Http\Response
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function show($slug)
|
||||
{
|
||||
$product = Product::findBySlug($slug);
|
||||
$product = ProductRepository::findBySlug($slug);
|
||||
$relatedProducts = $product->relatedProducts()->forCard()->get();
|
||||
$upSellProducts = $product->upSellProducts()->forCard()->get();
|
||||
$review = $this->getReviewData($product);
|
||||
|
||||
$product->append([
|
||||
'is_in_flash_sale',
|
||||
'flash_sale_end_date',
|
||||
'formatted_price_range',
|
||||
]);
|
||||
|
||||
event(new ProductViewed($product));
|
||||
|
||||
return view('public.products.show', compact('product', 'relatedProducts', 'upSellProducts', 'review'));
|
||||
}
|
||||
|
||||
|
||||
private function getReviewData(Product $product)
|
||||
{
|
||||
if (! setting('reviews_enabled')) {
|
||||
return;
|
||||
if (!setting('reviews_enabled')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Review::countAndAvgRating($product);
|
||||
|
||||
@@ -13,7 +13,8 @@ class ProductPriceController
|
||||
* Show the specified resource.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function show($id)
|
||||
{
|
||||
@@ -23,12 +24,12 @@ class ProductPriceController
|
||||
->findOrFail($id);
|
||||
|
||||
$variantPrice = $this->cartItem($product, request('options', []))
|
||||
->total()
|
||||
->totalPrice()
|
||||
->convertToCurrentCurrency()
|
||||
->format();
|
||||
|
||||
return product_price_formatted($product, function ($price) use ($product, $variantPrice) {
|
||||
if (! $product->hasSpecialPrice()) {
|
||||
if (!$product->hasSpecialPrice()) {
|
||||
return $variantPrice;
|
||||
}
|
||||
|
||||
@@ -36,6 +37,7 @@ class ProductPriceController
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private function cartItem(Product $product, array $options)
|
||||
{
|
||||
$chosenOptions = new ChosenProductOptions($product, $options);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Modules\Product\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Modules\Product\Entities\Product;
|
||||
use Modules\Category\Entities\Category;
|
||||
@@ -14,9 +15,10 @@ trait ProductSearch
|
||||
/**
|
||||
* Search products for the request.
|
||||
*
|
||||
* @param \Modules\Product\Entities\Product $model
|
||||
* @param \Modules\Product\Filters\ProductFilter $productFilter
|
||||
* @return \Illuminate\Http\Response
|
||||
* @param Product $model
|
||||
* @param ProductFilter $productFilter
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function searchProducts(Product $model, ProductFilter $productFilter)
|
||||
{
|
||||
@@ -43,9 +45,10 @@ trait ProductSearch
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
private function getAttributes($productIds)
|
||||
{
|
||||
if (! request()->filled('category') || $this->filteringViaRootCategory()) {
|
||||
if (!request()->filled('category') || $this->filteringViaRootCategory()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
@@ -57,6 +60,7 @@ trait ProductSearch
|
||||
->get();
|
||||
}
|
||||
|
||||
|
||||
private function filteringViaRootCategory()
|
||||
{
|
||||
return Category::where('slug', request('category'))
|
||||
@@ -64,6 +68,7 @@ trait ProductSearch
|
||||
->isRoot();
|
||||
}
|
||||
|
||||
|
||||
private function getProductsCategoryIds($productIds)
|
||||
{
|
||||
return DB::table('product_categories')
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
namespace Modules\Product\Http\Controllers;
|
||||
|
||||
use Closure;
|
||||
use Modules\Product\Entities\Product;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Modules\Product\Http\Response\SuggestionsResponse;
|
||||
|
||||
class SuggestionController
|
||||
@@ -11,9 +13,9 @@ class SuggestionController
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
* @return SuggestionsResponse
|
||||
*/
|
||||
public function index(Product $model)
|
||||
public function index(Product $model): SuggestionsResponse
|
||||
{
|
||||
$products = $this->getProducts($model);
|
||||
|
||||
@@ -25,25 +27,13 @@ class SuggestionController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total results count.
|
||||
*
|
||||
* @param \Modules\Product\Entities\Product $model
|
||||
* @return int
|
||||
*/
|
||||
private function getTotalResults(Product $model)
|
||||
{
|
||||
return $model->search(request('query'))
|
||||
->query()
|
||||
->when(request()->filled('category'), $this->categoryQuery())
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get products suggestions.
|
||||
*
|
||||
* @param \Modules\Product\Entities\Product $model
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
* @param Product $model
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
private function getProducts(Product $model)
|
||||
{
|
||||
@@ -67,10 +57,11 @@ class SuggestionController
|
||||
->get();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns categories condition closure.
|
||||
*
|
||||
* @return \Closure
|
||||
* @return Closure
|
||||
*/
|
||||
private function categoryQuery()
|
||||
{
|
||||
@@ -80,4 +71,20 @@ class SuggestionController
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get totalPrice results count.
|
||||
*
|
||||
* @param Product $model
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function getTotalResults(Product $model): int
|
||||
{
|
||||
return $model->search(request('query'))
|
||||
->query()
|
||||
->when(request()->filled('category'), $this->categoryQuery())
|
||||
->count();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,9 @@ class SetProductSortOption
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @param Request $request
|
||||
* @param Closure $next
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
@@ -27,25 +28,29 @@ class SetProductSortOption
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the request should set "relevance" sort option.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param Request $request
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function shouldSetRelevanceSortOption($request)
|
||||
{
|
||||
return $request->has('query') && ! $request->has('sort');
|
||||
return $request->has('query') && !$request->has('sort');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the request should set "latest" sort option.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param Request $request
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function shouldSetLatestSortOption($request)
|
||||
{
|
||||
return ! $request->has('query') && ! $request->has('sort');
|
||||
return !$request->has('query') && !$request->has('sort');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
namespace Modules\Product\Http\Requests;
|
||||
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\Option\Entities\Option;
|
||||
use Modules\Product\Entities\Product;
|
||||
use Modules\Core\Http\Requests\Request;
|
||||
use Modules\Variation\Entities\Variation;
|
||||
use Modules\Product\Rules\DistinctProductVariationValueLabel;
|
||||
|
||||
class SaveProductRequest extends Request
|
||||
{
|
||||
@@ -15,35 +18,126 @@ class SaveProductRequest extends Request
|
||||
*/
|
||||
protected $availableAttributes = 'product::attributes';
|
||||
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
public function rules(): array
|
||||
{
|
||||
return array_merge(
|
||||
$this->getProductRules(),
|
||||
$this->getProductAttributeRules(),
|
||||
$this->getProductVariationsRules(),
|
||||
$this->getProductVariantsRules(),
|
||||
$this->getProductOptionsRules(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function getProductRules(): array
|
||||
{
|
||||
return array_merge(
|
||||
[
|
||||
'slug' => $this->getSlugRules(),
|
||||
'name' => 'required',
|
||||
'description' => 'required',
|
||||
'brand_id' => ['nullable', Rule::exists('brands', 'id')],
|
||||
'tax_class_id' => ['nullable', Rule::exists('tax_classes', 'id')],
|
||||
'price' => 'required_without:variants|nullable|numeric|min:0|max:99999999999999',
|
||||
'special_price' => 'nullable|numeric|min:0|max:99999999999999',
|
||||
'special_price_type' => ['nullable', Rule::in(['fixed', 'percent'])],
|
||||
'special_price_start' => 'nullable|date|before:special_price_end',
|
||||
'special_price_end' => 'nullable|date|after:special_price_start',
|
||||
'manage_stock' => 'required|boolean',
|
||||
'qty' => 'required_if:manage_stock,1|nullable|numeric',
|
||||
'in_stock' => 'required|boolean',
|
||||
'new_from' => 'nullable|date',
|
||||
'new_to' => 'nullable|date',
|
||||
'is_virtual' => 'required|boolean',
|
||||
'is_active' => 'required|boolean',
|
||||
],
|
||||
$this->getInventoryRules()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function getInventoryRules(): array
|
||||
{
|
||||
if (!$this->request->has('variations')) {
|
||||
return [
|
||||
'manage_stock' => 'required|boolean',
|
||||
'qty' => 'required_if:manage_stock,1|nullable|numeric',
|
||||
'in_stock' => 'required|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
public function getProductAttributeRules(): array
|
||||
{
|
||||
return [
|
||||
'slug' => $this->getSlugRules(),
|
||||
'name' => 'required',
|
||||
'description' => 'required',
|
||||
'brand_id' => ['nullable', Rule::exists('brands', 'id')],
|
||||
'tax_class_id' => ['nullable', Rule::exists('tax_classes', 'id')],
|
||||
'is_virtual' => 'required|boolean',
|
||||
'is_active' => 'required|boolean',
|
||||
'price' => 'required|numeric|min:0|max:99999999999999',
|
||||
'special_price' => 'nullable|numeric|min:0|max:99999999999999',
|
||||
'special_price_type' => ['nullable', Rule::in(['fixed', 'percent'])],
|
||||
'special_price_start' => 'nullable|date',
|
||||
'special_price_end' => 'nullable|date',
|
||||
'manage_stock' => 'required|boolean',
|
||||
'qty' => 'required_if:manage_stock,1|nullable|numeric',
|
||||
'in_stock' => 'required|boolean',
|
||||
'new_from' => 'nullable|date',
|
||||
'new_to' => 'nullable|date',
|
||||
'attributes.*.attribute_id' => ['required_with:attributes.*.values', Rule::exists('attributes', 'id')],
|
||||
'attributes.*.values' => ['required_with:attributes.*.attribute_id', Rule::exists('attribute_values', 'id')],
|
||||
];
|
||||
}
|
||||
|
||||
private function getSlugRules()
|
||||
|
||||
public function getProductVariationsRules(): array
|
||||
{
|
||||
return [
|
||||
'variations.*.name' => 'required_with:variations.*.type',
|
||||
'variations.*.type' => ['nullable', 'required_with:variations.*.name', Rule::in(Variation::TYPES)],
|
||||
'variations.*.values.*.label' => ['required_with:variations.*.type', new DistinctProductVariationValueLabel()],
|
||||
'variations.*.values.*.color' => ['required_if:variations.*.type,color', 'regex:/^#(?:[0-9a-fA-F]{3}){1,2}$/'],
|
||||
'variations.*.values.*.image' => 'required_if:type,image|integer|min:1',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public function getProductVariantsRules(): array
|
||||
{
|
||||
return [
|
||||
'variants.*.name' => 'required',
|
||||
'variants.*.sku' => 'nullable',
|
||||
'variants.*.price' => 'required_if:variants.*.is_active,true|nullable|numeric|min:0|max:99999999999999',
|
||||
'variants.*.special_price' => 'nullable|numeric|min:0|max:99999999999999',
|
||||
'variants.*.special_price_type' => ['nullable', Rule::in(['fixed', 'percent'])],
|
||||
'variants.*.special_price_start' => 'nullable|date|before:variants.*.special_price_end',
|
||||
'variants.*.special_price_end' => 'nullable|date|after:variants.*.special_price_start',
|
||||
'variants.*.manage_stock' => 'required_if:variants.*.is_active,1|boolean',
|
||||
'variants.*.qty' => 'required_if:variants.*.is_active,1|required_if:variants.*.manage_stock,1|nullable|numeric',
|
||||
'variants.*.in_stock' => 'required_if:variants.*.is_active,1|boolean',
|
||||
'variants.*.is_active' => 'required|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public function getProductOptionsRules(): array
|
||||
{
|
||||
return [
|
||||
'options.*.name' => 'required_with:options.*.type',
|
||||
'options.*.type' => ['nullable', 'required_with:options.*.name', Rule::in(Option::TYPES)],
|
||||
'options.*.is_required' => ['required_with:options.*.name', 'boolean'],
|
||||
'options.*.values.*.label' => 'required_if:options.*.type,dropdown,checkbox,checkbox_custom,radio,radio_custom,multiple_select',
|
||||
'options.*.values.*.price' => 'nullable|numeric|min:0|max:99999999999999',
|
||||
'options.*.values.*.price_type' => ['required', Rule::in(['fixed', 'percent'])],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public function messages()
|
||||
{
|
||||
return array_merge(parent::messages(), [
|
||||
'price.required_without' => trans('product::validation.price_field_is_required'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
private function getSlugRules(): array
|
||||
{
|
||||
$rules = $this->route()->getName() === 'admin.products.update' ? ['required'] : ['sometimes'];
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace Modules\Product\Http\Response;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Collection;
|
||||
use Modules\Product\Entities\Product;
|
||||
use Modules\Category\Entities\Category;
|
||||
@@ -9,20 +11,21 @@ use Illuminate\Contracts\Support\Responsable;
|
||||
|
||||
class SuggestionsResponse implements Responsable
|
||||
{
|
||||
private $query;
|
||||
private $products;
|
||||
private $categories;
|
||||
private $totalResults;
|
||||
private string $query;
|
||||
private Collection $products;
|
||||
private Collection $categories;
|
||||
private int $totalResults;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
*
|
||||
* @param string $query
|
||||
* @param int $totalResults
|
||||
* @param \Illuminate\Support\Collection $products
|
||||
* @param \Illuminate\Support\Collection $categories
|
||||
* @param Collection $products
|
||||
* @param Collection $categories
|
||||
*/
|
||||
public function __construct($query, Collection $products, Collection $categories, $totalResults)
|
||||
public function __construct(string $query, Collection $products, Collection $categories, int $totalResults)
|
||||
{
|
||||
$this->query = $query;
|
||||
$this->products = $products;
|
||||
@@ -30,13 +33,15 @@ class SuggestionsResponse implements Responsable
|
||||
$this->totalResults = $totalResults;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create an HTTP response that represents the object.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
* @param Request $request
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function toResponse($request)
|
||||
public function toResponse($request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'categories' => $this->transformCategories(),
|
||||
@@ -45,12 +50,13 @@ class SuggestionsResponse implements Responsable
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Transform the categories.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection
|
||||
* @return Collection
|
||||
*/
|
||||
private function transformCategories()
|
||||
private function transformCategories(): Collection
|
||||
{
|
||||
return $this->categories->map(function (Category $category) {
|
||||
return [
|
||||
@@ -61,44 +67,48 @@ class SuggestionsResponse implements Responsable
|
||||
})->unique('slug')->values();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Transform the products.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection
|
||||
* @return Collection
|
||||
*/
|
||||
private function transformProducts()
|
||||
private function transformProducts(): Collection
|
||||
{
|
||||
return $this->products->map(function (Product $product) {
|
||||
return [
|
||||
'slug' => $product->slug,
|
||||
'name' => $this->highlight($product->name),
|
||||
'formatted_price' => $product->getFormattedPriceAttribute(),
|
||||
'base_image' => $product->getBaseImageAttribute(),
|
||||
'is_out_of_stock' => $product->isOutOfStock(),
|
||||
'url' => $product->url(),
|
||||
'formatted_price' => $product->variant?->formatted_price ?? $product->formatted_price,
|
||||
'base_image' => $product->variant?->base_image ?? $product->base_image,
|
||||
'is_out_of_stock' => $product->variant?->isOutOfStock() ?? $product->isOutOfStock(),
|
||||
'url' => $product->variant?->url() ?? $product->url(),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Highlight the given text.
|
||||
*
|
||||
* @param string $text
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function highlight($text)
|
||||
private function highlight($text): string
|
||||
{
|
||||
$query = str_replace(' ', '|', preg_quote($this->query));
|
||||
|
||||
return preg_replace("/($query)/i", '<em>$1</em>', $text);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get remaining results count.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function getRemainingCount()
|
||||
private function getRemainingCount(): int
|
||||
{
|
||||
return $this->totalResults - $this->products->count();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Http\ViewComposers;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Modules\Tag\Entities\Tag;
|
||||
use Modules\Brand\Entities\Brand;
|
||||
use Modules\Tax\Entities\TaxClass;
|
||||
use Modules\Option\Entities\Option;
|
||||
use Modules\Category\Entities\Category;
|
||||
use Modules\Variation\Entities\Variation;
|
||||
use Modules\Attribute\Entities\AttributeSet;
|
||||
|
||||
class ProductCreatePageComposer
|
||||
{
|
||||
/**
|
||||
* Bind data to the view.
|
||||
*
|
||||
* @param View $view
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function compose(View $view)
|
||||
{
|
||||
$view->with([
|
||||
'globalVariations' => Variation::globals()->latest()->get(),
|
||||
'globalOptions' => Option::globals()->latest()->get(),
|
||||
'brands' => Brand::list()->prepend(trans('admin::admin.form.please_select'), ''),
|
||||
'categories' => Category::treeList(),
|
||||
'taxClasses' => TaxClass::list()->prepend(trans('admin::admin.form.please_select'), ''),
|
||||
'tags' => Tag::list(),
|
||||
'attributeSets' => AttributeSet::with('attributes.values')->get()->sortBy('name'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Http\ViewComposers;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Modules\Tag\Entities\Tag;
|
||||
use Modules\Brand\Entities\Brand;
|
||||
use Modules\Tax\Entities\TaxClass;
|
||||
use Modules\Option\Entities\Option;
|
||||
use Modules\Category\Entities\Category;
|
||||
use Modules\Variation\Entities\Variation;
|
||||
use Modules\Attribute\Entities\AttributeSet;
|
||||
|
||||
class ProductEditPageComposer
|
||||
{
|
||||
/**
|
||||
* Bind data to the view.
|
||||
*
|
||||
* @param View $view
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function compose(View $view)
|
||||
{
|
||||
$view->with([
|
||||
'globalVariations' => Variation::globals()->latest()->get(),
|
||||
'globalOptions' => Option::globals()->latest()->get(),
|
||||
'brands' => Brand::list()->prepend(trans('admin::admin.form.please_select'), ''),
|
||||
'categories' => Category::treeList(),
|
||||
'taxClasses' => TaxClass::list()->prepend(trans('admin::admin.form.please_select'), ''),
|
||||
'tags' => Tag::list(),
|
||||
'attributeSets' => AttributeSet::with('attributes.values')->get()->sortBy('name'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -11,31 +11,34 @@ class AddToRecentlyViewed
|
||||
/**
|
||||
* The recently viewed instance.
|
||||
*
|
||||
* @var \Modules\Product\RecentlyViewed
|
||||
* @var RecentlyViewed
|
||||
*/
|
||||
private $recentlyViewed;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new event listener.
|
||||
*
|
||||
* @param \Modules\Product\RecentlyViewed $recentlyViewed
|
||||
* @param RecentlyViewed $recentlyViewed
|
||||
*/
|
||||
public function __construct(RecentlyViewed $recentlyViewed)
|
||||
{
|
||||
$this->recentlyViewed = $recentlyViewed;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param \Modules\Product\Events\ProductViewed $event
|
||||
* @param ProductViewed $event
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle(ProductViewed $event)
|
||||
{
|
||||
try {
|
||||
$this->recentlyViewed->store($event->product);
|
||||
} catch (Exception $e) {
|
||||
} catch (Exception) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ class IncrementProductView
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param \Modules\Product\Events\ProductViewed $event
|
||||
* @param ProductViewed $event
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle(ProductViewed $event)
|
||||
|
||||
54
Modules/Product/Listeners/SaveProductVariants.php
Normal file
54
Modules/Product/Listeners/SaveProductVariants.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Listeners;
|
||||
|
||||
use Modules\Product\Entities\Product;
|
||||
|
||||
class SaveProductVariants
|
||||
{
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param Product $product
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle($product)
|
||||
{
|
||||
$ids = $this->getDeleteCandidates($product);
|
||||
|
||||
if ($ids->isNotEmpty()) {
|
||||
$product->variants()->forceDelete($ids);
|
||||
}
|
||||
|
||||
$this->saveVariants($product);
|
||||
}
|
||||
|
||||
|
||||
private function getDeleteCandidates($product)
|
||||
{
|
||||
return $product
|
||||
->variants()
|
||||
->withoutGlobalScope('active')
|
||||
->pluck('id')
|
||||
->diff(array_pluck($this->variants(), 'id'));
|
||||
}
|
||||
|
||||
|
||||
private function variants()
|
||||
{
|
||||
return request('variants', []);
|
||||
}
|
||||
|
||||
|
||||
private function saveVariants($product)
|
||||
{
|
||||
$variants = $this->variants();
|
||||
$counter = 0;
|
||||
|
||||
foreach ($variants as $attributes) {
|
||||
$attributes['position'] = ++$counter;
|
||||
$product->variants()->withoutGlobalScope('active')->updateOrCreate(['id' => $attributes['id'] ?? null], $attributes);
|
||||
}
|
||||
}
|
||||
}
|
||||
63
Modules/Product/Listeners/SaveProductVariations.php
Normal file
63
Modules/Product/Listeners/SaveProductVariations.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Listeners;
|
||||
|
||||
use Modules\Product\Entities\Product;
|
||||
|
||||
class SaveProductVariations
|
||||
{
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param Product $product
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle(Product $product): void
|
||||
{
|
||||
$ids = $this->getDeleteCandidates($product);
|
||||
|
||||
if ($ids->isNotEmpty()) {
|
||||
$product->variations()->detach($ids);
|
||||
}
|
||||
|
||||
$this->saveVariations($product);
|
||||
}
|
||||
|
||||
|
||||
private function getDeleteCandidates($product)
|
||||
{
|
||||
return $product
|
||||
->variations()
|
||||
->pluck('id')
|
||||
->diff(array_pluck($this->variations(), 'id'));
|
||||
}
|
||||
|
||||
|
||||
private function variations()
|
||||
{
|
||||
return array_filter(request('variations', []), function ($variation) {
|
||||
return !is_null($variation['name']);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private function saveVariations($product): void
|
||||
{
|
||||
$counter = 0;
|
||||
|
||||
foreach (array_reset_index($this->variations()) as $attributes) {
|
||||
#if it's a global variation make the id null to render it creatable
|
||||
if ($attributes['is_global'] === true) {
|
||||
$attributes['id'] = null;
|
||||
}
|
||||
|
||||
$attributes['is_global'] = false;
|
||||
$attributes['position'] = ++$counter;
|
||||
|
||||
$variation = $product->variations()->updateOrCreate(['id' => $attributes['id'] ?? null], $attributes);
|
||||
|
||||
$variation->saveValues($attributes['values'] ?? []);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,13 @@ class StoreSearchTerm
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param \Modules\Product\Events\ShowingProductList $event
|
||||
* @param ShowingProductList $event
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle(ShowingProductList $event)
|
||||
{
|
||||
if (! request()->filled('query')) {
|
||||
if (!request()->filled('query')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
namespace Modules\Product\Providers;
|
||||
|
||||
use Modules\Product\Entities\Product;
|
||||
use Modules\Product\Events\ProductViewed;
|
||||
use Modules\Product\Listeners\StoreSearchTerm;
|
||||
use Modules\Product\Events\ShowingProductList;
|
||||
use Modules\Product\Listeners\SaveProductVariants;
|
||||
use Modules\Product\Listeners\AddToRecentlyViewed;
|
||||
use Modules\Product\Listeners\IncrementProductView;
|
||||
use Modules\Product\Listeners\SaveProductVariations;
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
@@ -12,12 +20,26 @@ class EventServiceProvider extends ServiceProvider
|
||||
* @var array
|
||||
*/
|
||||
protected $listen = [
|
||||
\Modules\Product\Events\ProductViewed::class => [
|
||||
\Modules\Product\Listeners\IncrementProductView::class,
|
||||
\Modules\Product\Listeners\AddToRecentlyViewed::class,
|
||||
ProductViewed::class => [
|
||||
IncrementProductView::class,
|
||||
AddToRecentlyViewed::class,
|
||||
],
|
||||
\Modules\Product\Events\ShowingProductList::class => [
|
||||
\Modules\Product\Listeners\StoreSearchTerm::class,
|
||||
ShowingProductList::class => [
|
||||
StoreSearchTerm::class,
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Bootstrap the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
Product::saved(SaveProductVariations::class);
|
||||
Product::saved(SaveProductVariants::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,35 +3,31 @@
|
||||
namespace Modules\Product\Providers;
|
||||
|
||||
use Modules\Product\RecentlyViewed;
|
||||
use Modules\Support\Traits\AddsAsset;
|
||||
use Modules\Product\Admin\ProductTabs;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\Admin\Ui\Facades\TabManager;
|
||||
use Modules\Product\Http\ViewComposers\ProductEditPageComposer;
|
||||
use Modules\Product\Http\ViewComposers\ProductCreatePageComposer;
|
||||
|
||||
class ProductServiceProvider extends ServiceProvider
|
||||
{
|
||||
use AddsAsset;
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
public function boot(): void
|
||||
{
|
||||
TabManager::register('products', ProductTabs::class);
|
||||
|
||||
$this->addAdminAssets('admin.products.(create|edit)', [
|
||||
'admin.media.css', 'admin.media.js', 'admin.product.css', 'admin.product.js',
|
||||
]);
|
||||
View::composer('product::admin.products.create', ProductCreatePageComposer::class);
|
||||
View::composer('product::admin.products.edit', ProductEditPageComposer::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Register the service provider.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(RecentlyViewed::class, function ($app) {
|
||||
return new RecentlyViewed(
|
||||
|
||||
@@ -3,10 +3,14 @@
|
||||
namespace Modules\Product;
|
||||
|
||||
use Darryldecode\Cart\Cart as DarryldecodeCart;
|
||||
use Darryldecode\Cart\Exceptions\InvalidItemException;
|
||||
|
||||
class RecentlyViewed extends DarryldecodeCart
|
||||
{
|
||||
public function store($product)
|
||||
/**
|
||||
* @throws InvalidItemException
|
||||
*/
|
||||
public function store($product): RecentlyViewed
|
||||
{
|
||||
$product->load('reviews');
|
||||
|
||||
@@ -21,6 +25,7 @@ class RecentlyViewed extends DarryldecodeCart
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function products()
|
||||
{
|
||||
return $this->getContent()->map(function ($item) {
|
||||
|
||||
40
Modules/Product/Repositories/ProductRepository.php
Normal file
40
Modules/Product/Repositories/ProductRepository.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Repositories;
|
||||
|
||||
use Modules\Product\Entities\Product;
|
||||
|
||||
class ProductRepository
|
||||
{
|
||||
public static function list($ids = [])
|
||||
{
|
||||
return Product::select('id')
|
||||
->withName()
|
||||
->whereIn('id', $ids)
|
||||
->when(!empty($ids), function ($query) use ($ids) {
|
||||
$idsString = collect($ids)
|
||||
->filter()
|
||||
->implode(',');
|
||||
|
||||
$query->orderByRaw("FIELD(id, {$idsString})");
|
||||
})
|
||||
->get()
|
||||
->mapWithKeys(function ($product) {
|
||||
return [$product->id => $product->name];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public static function findBySlug($slug)//: Model|Builder
|
||||
{
|
||||
$product = Product::with(['variations', 'variants', 'categories', 'tags', 'attributes.attribute.attributeSet', 'options', 'files', 'relatedProducts', 'upSellProducts'])
|
||||
->where('slug', $slug)
|
||||
->firstOrFail();
|
||||
|
||||
if (request()->query('variant') && !$product->variant) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return $product;
|
||||
}
|
||||
}
|
||||
153
Modules/Product/Resources/assets/admin/js/create.js
Normal file
153
Modules/Product/Resources/assets/admin/js/create.js
Normal file
@@ -0,0 +1,153 @@
|
||||
import Vue from "vue";
|
||||
import ProductMixin from "./mixins/ProductMixin";
|
||||
import Errors from "@admin/js/Errors";
|
||||
import { generateSlug } from "@admin/js/functions";
|
||||
|
||||
Vue.prototype.route = route;
|
||||
|
||||
new Vue({
|
||||
el: "#app",
|
||||
|
||||
mixins: [ProductMixin],
|
||||
|
||||
data: {
|
||||
formSubmissionType: null,
|
||||
form: {
|
||||
brand_id: "",
|
||||
tax_class_id: "",
|
||||
is_active: true,
|
||||
media: [],
|
||||
is_virtual: false,
|
||||
manage_stock: 0,
|
||||
in_stock: 1,
|
||||
special_price_type: "fixed",
|
||||
meta: {},
|
||||
attributes: [],
|
||||
downloads: [],
|
||||
variations: [],
|
||||
variants: [],
|
||||
options: [],
|
||||
slug: null,
|
||||
},
|
||||
errors: new Errors(),
|
||||
selectizeConfig: {
|
||||
plugins: ["remove_button"],
|
||||
},
|
||||
searchableSelectizeConfig: {},
|
||||
categoriesSelectizeConfig: {
|
||||
plugins: ["remove_button"],
|
||||
onItemAdd(value) {
|
||||
this.getItem(value)[0].innerHTML = this.getItem(
|
||||
value
|
||||
)[0].innerHTML.replace(/¦––\s/g, "");
|
||||
},
|
||||
},
|
||||
flatPickrConfig: {
|
||||
mode: "single",
|
||||
enableTime: true,
|
||||
altInput: true,
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.setSearchableSelectizeConfig();
|
||||
},
|
||||
|
||||
methods: {
|
||||
setProductSlug(value) {
|
||||
this.form.slug = generateSlug(value);
|
||||
},
|
||||
|
||||
setFormDefaultData() {
|
||||
this.form = {
|
||||
brand_id: "",
|
||||
tax_class_id: "",
|
||||
is_active: true,
|
||||
media: [],
|
||||
is_virtual: false,
|
||||
manage_stock: 0,
|
||||
in_stock: 1,
|
||||
special_price_type: "fixed",
|
||||
meta: {},
|
||||
attributes: [],
|
||||
downloads: [],
|
||||
variations: [],
|
||||
variants: [],
|
||||
options: [],
|
||||
slug: null,
|
||||
};
|
||||
},
|
||||
|
||||
resetForm() {
|
||||
this.setFormDefaultData();
|
||||
tinyMCE.get("description").setContent("");
|
||||
this.resetBulkEditVariantFields();
|
||||
|
||||
this.focusField({
|
||||
selector: "#name",
|
||||
});
|
||||
},
|
||||
|
||||
submit({ submissionType }) {
|
||||
this.formSubmissionType = submissionType;
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: route("admin.products.store", {
|
||||
...((submissionType === "save_and_edit" ||
|
||||
submissionType === "save_and_exit") && {
|
||||
exit_flash: true,
|
||||
}),
|
||||
}),
|
||||
data: this.transformData(this.form),
|
||||
dataType: "json",
|
||||
success: ({ message, product_id }) => {
|
||||
if (submissionType === "save_and_edit") {
|
||||
location.href = route(
|
||||
"admin.products.edit",
|
||||
product_id
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (submissionType === "save_and_exit") {
|
||||
location.href = route("admin.products.index");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
toaster(message, {
|
||||
type: "success",
|
||||
});
|
||||
|
||||
this.resetForm();
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
this.formSubmissionType = null;
|
||||
|
||||
toaster(error.responseJSON.message, {
|
||||
type: "default",
|
||||
});
|
||||
|
||||
if (error.status === 422) {
|
||||
this.errors.reset();
|
||||
this.errors.record(error.responseJSON.errors);
|
||||
this.scrollToFirstErrorField(this.$refs.form.elements);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.hasAnyVariant) {
|
||||
this.regenerateVariationsAndVariantsUid();
|
||||
}
|
||||
})
|
||||
.always(() => {
|
||||
if (submissionType === "save") {
|
||||
this.formSubmissionType = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
145
Modules/Product/Resources/assets/admin/js/edit.js
Normal file
145
Modules/Product/Resources/assets/admin/js/edit.js
Normal file
@@ -0,0 +1,145 @@
|
||||
import Vue from "vue";
|
||||
import ProductMixin from "./mixins/ProductMixin";
|
||||
import Errors from "@admin/js/Errors";
|
||||
|
||||
Vue.prototype.route = route;
|
||||
|
||||
new Vue({
|
||||
el: "#app",
|
||||
|
||||
mixins: [ProductMixin],
|
||||
|
||||
data: {
|
||||
formSubmissionType: null,
|
||||
form: {
|
||||
brand_id: "",
|
||||
categories: [],
|
||||
tags: [],
|
||||
media: [],
|
||||
name: "lorem ipsum",
|
||||
description: "<p>lorem ipsum dolor</p>",
|
||||
is_active: 1,
|
||||
price: 999,
|
||||
tax_class_id: "",
|
||||
is_virtual: 0,
|
||||
manage_stock: 0,
|
||||
in_stock: 1,
|
||||
special_price_type: "fixed",
|
||||
meta: {},
|
||||
variations: [],
|
||||
variants: [],
|
||||
attributes: [],
|
||||
options: [],
|
||||
downloads: [],
|
||||
},
|
||||
errors: new Errors(),
|
||||
selectizeConfig: {
|
||||
plugins: ["remove_button"],
|
||||
},
|
||||
searchableSelectizeConfig: {},
|
||||
categoriesSelectizeConfig: {
|
||||
plugins: ["remove_button"],
|
||||
onItemAdd(value) {
|
||||
this.getItem(value)[0].innerHTML = this.getItem(
|
||||
value
|
||||
)[0].innerHTML.replace(/¦––\s/g, "");
|
||||
},
|
||||
},
|
||||
flatPickrConfig: {
|
||||
mode: "single",
|
||||
enableTime: true,
|
||||
altInput: true,
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.setFormData();
|
||||
this.setSearchableSelectizeConfig();
|
||||
this.setDefaultVariantUid();
|
||||
this.setVariantsLength();
|
||||
},
|
||||
|
||||
methods: {
|
||||
prepareFormData(formData) {
|
||||
this.prepareAttributes(formData.attributes);
|
||||
this.prepareVariations(formData.variations);
|
||||
this.prepareVariants(formData.variants);
|
||||
this.prepareOptions(formData.options);
|
||||
|
||||
return formData;
|
||||
},
|
||||
|
||||
setFormData() {
|
||||
this.form = { ...this.prepareFormData(FleetCart.data["product"]) };
|
||||
},
|
||||
|
||||
setDefaultVariantUid() {
|
||||
if (this.hasAnyVariant) {
|
||||
this.defaultVariantUid = this.form.variants.find(
|
||||
({ is_default }) => is_default === true
|
||||
).uid;
|
||||
}
|
||||
},
|
||||
|
||||
setVariantsLength() {
|
||||
this.variantsLength = this.form.variants.length;
|
||||
},
|
||||
|
||||
submit({ submissionType }) {
|
||||
this.formSubmissionType = submissionType;
|
||||
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
url: route("admin.products.update", {
|
||||
id: this.form.id,
|
||||
...(submissionType === "save_and_exit" && {
|
||||
exit_flash: true,
|
||||
}),
|
||||
}),
|
||||
data: this.transformData(this.form),
|
||||
dataType: "json",
|
||||
success: (response) => {
|
||||
if (submissionType === "save_and_exit") {
|
||||
location.href = route("admin.products.index");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.form = { ...response.product_resource };
|
||||
|
||||
this.errors.reset();
|
||||
this.prepareFormData(this.form);
|
||||
this.resetBulkEditVariantFields();
|
||||
|
||||
toaster(response.message, {
|
||||
type: "success",
|
||||
});
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
this.formSubmissionType = null;
|
||||
|
||||
toaster(error.responseJSON.message, {
|
||||
type: "default",
|
||||
});
|
||||
|
||||
if (error.status === 422) {
|
||||
this.errors.reset();
|
||||
this.errors.record(error.responseJSON.errors);
|
||||
this.scrollToFirstErrorField(this.$refs.form.elements);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.hasAnyVariant) {
|
||||
this.regenerateVariationsAndVariantsUid();
|
||||
}
|
||||
})
|
||||
.always(() => {
|
||||
if (submissionType === "save") {
|
||||
this.formSubmissionType = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
export default {
|
||||
watch: {
|
||||
"form.attributes": {
|
||||
immediate: true,
|
||||
handler(newValue) {
|
||||
if (newValue.length === 0) {
|
||||
this.addAttribute();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
prepareAttributes(attributes) {
|
||||
attributes.forEach((attribute) => {
|
||||
this.$set(attribute, "uid", this.uid());
|
||||
});
|
||||
},
|
||||
|
||||
getAttributeValuesById(id) {
|
||||
let values = null;
|
||||
let matched = false;
|
||||
|
||||
if (id === "") return;
|
||||
|
||||
Object.values(FleetCart.data["attribute-sets"]).some(
|
||||
(attributeSet) => {
|
||||
attributeSet.attributes.some((attribute) => {
|
||||
if (attribute.id === id) {
|
||||
matched = true;
|
||||
values = attribute.values;
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (matched) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return values;
|
||||
},
|
||||
|
||||
focusAttributeValueField(index) {
|
||||
this.$refs.attributeValues[index].$el.selectize.focus();
|
||||
},
|
||||
|
||||
addAttribute() {
|
||||
this.form.attributes.push({
|
||||
attribute_id: "",
|
||||
uid: this.uid(),
|
||||
values: [],
|
||||
});
|
||||
},
|
||||
|
||||
deleteAttribute(index, uid) {
|
||||
this.form.attributes.splice(index, 1);
|
||||
this.clearErrors({ name: "attributes", uid });
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,224 @@
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
bulkEditVariantsUid: "",
|
||||
bulkEditVariantsField: "",
|
||||
bulkEditVariants: {},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
hasBulkEditVariantsUid() {
|
||||
return this.bulkEditVariantsUid !== "";
|
||||
},
|
||||
|
||||
hasBulkEditVariantsField() {
|
||||
return this.bulkEditVariantsField !== "";
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
changeBulkEditVariantsUid(uid) {
|
||||
this.resetVariantsSelection();
|
||||
|
||||
if (uid === "") {
|
||||
this.resetBulkEditVariantsField();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectVariants(uid);
|
||||
},
|
||||
|
||||
selectVariants(uid) {
|
||||
this.resetVariantsSelection();
|
||||
|
||||
if (uid === "") return;
|
||||
|
||||
if (uid !== "all") {
|
||||
this.selectSpecificVariants(uid);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectAllVariants();
|
||||
},
|
||||
|
||||
selectAllVariants() {
|
||||
this.form.variants.forEach((variant) => {
|
||||
this.$set(variant, "is_selected", true);
|
||||
});
|
||||
},
|
||||
|
||||
selectSpecificVariants(uid) {
|
||||
this.form.variants.forEach((variant) => {
|
||||
if (variant.uids.includes(uid)) {
|
||||
this.$set(variant, "is_selected", true);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
changeBulkEditVariantsField(fieldName) {
|
||||
const focusableFieldNames = ["sku", "price", "special_price"];
|
||||
|
||||
if (focusableFieldNames.includes(fieldName)) {
|
||||
this.focusField({
|
||||
selector: `#bulk-edit-variants-${fieldName.replace(
|
||||
/_/g,
|
||||
"-"
|
||||
)}`,
|
||||
});
|
||||
}
|
||||
|
||||
this.resetBulkEditVariants();
|
||||
},
|
||||
|
||||
addBulkEditVariantsMedia() {
|
||||
const picker = new MediaPicker({ type: "image", multiple: true });
|
||||
|
||||
picker.on("select", ({ id, path }) => {
|
||||
this.bulkEditVariants.media.push({
|
||||
id: +id,
|
||||
path,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
removeBulkEditVariantsMedia(index) {
|
||||
this.bulkEditVariants.media.splice(index, 1);
|
||||
},
|
||||
|
||||
bulkEditvariantsDefaultData() {
|
||||
return {
|
||||
is_active: true,
|
||||
media: [],
|
||||
price: null,
|
||||
special_price: null,
|
||||
special_price_type: "fixed",
|
||||
special_price_start: null,
|
||||
special_price_end: null,
|
||||
manage_stock: 0,
|
||||
qty: null,
|
||||
in_stock: 1,
|
||||
};
|
||||
},
|
||||
|
||||
resetBulkEditVariantFields() {
|
||||
this.bulkEditVariantsUid = "";
|
||||
|
||||
this.resetVariantsSelection();
|
||||
this.resetBulkEditVariantsField();
|
||||
},
|
||||
|
||||
resetBulkEditVariantsField() {
|
||||
this.bulkEditVariantsField = "";
|
||||
|
||||
this.resetBulkEditVariants();
|
||||
},
|
||||
|
||||
resetBulkEditVariants() {
|
||||
this.bulkEditVariants = {
|
||||
...this.bulkEditvariantsDefaultData(),
|
||||
};
|
||||
},
|
||||
|
||||
resetVariantsSelection() {
|
||||
this.form.variants.forEach((variant) => {
|
||||
this.$set(variant, "is_selected", false);
|
||||
});
|
||||
},
|
||||
|
||||
clearVariantsSpecialPriceErrors(uid) {
|
||||
Object.keys(this.errors).forEach((key) => {
|
||||
if (
|
||||
key.startsWith(`variants.${uid}`) &&
|
||||
key.includes("special_price")
|
||||
) {
|
||||
this.errors.clear(key);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
updateVariantsField(variant, { key, value }) {
|
||||
this.$set(variant, key, value);
|
||||
|
||||
this.errors.clear(`variants.${variant.uid}.${key}`);
|
||||
},
|
||||
|
||||
updateVariantsStatus(variant, { key, value }) {
|
||||
if (variant.is_default === true) return;
|
||||
|
||||
this.$set(variant, key, value);
|
||||
|
||||
this.errors.clear(`variants.${variant.uid}.${key}`);
|
||||
},
|
||||
|
||||
updateVariantsSpecialPrice(
|
||||
variant,
|
||||
{ key, value },
|
||||
{ special_price_type, special_price_start, special_price_end }
|
||||
) {
|
||||
this.$set(variant, key, value);
|
||||
this.$set(variant, "special_price_type", special_price_type);
|
||||
this.$set(variant, "special_price_start", special_price_start);
|
||||
this.$set(variant, "special_price_end", special_price_end);
|
||||
|
||||
this.clearVariantsSpecialPriceErrors(variant.uid);
|
||||
},
|
||||
|
||||
updateVariantsManageStock(variant, { key, value }, { qty }) {
|
||||
this.$set(variant, key, value);
|
||||
this.$set(variant, "qty", qty);
|
||||
|
||||
this.errors.clear([
|
||||
`variants.${variant.uid}.${key}`,
|
||||
`variants.${variant.uid}.qty`,
|
||||
]);
|
||||
},
|
||||
|
||||
callUpdateVariantsMethodByField(key) {
|
||||
return {
|
||||
media: this.updateVariantsField,
|
||||
sku: this.updateVariantsField,
|
||||
is_active: this.updateVariantsStatus,
|
||||
price: this.updateVariantsField,
|
||||
special_price: this.updateVariantsSpecialPrice,
|
||||
manage_stock: this.updateVariantsManageStock,
|
||||
in_stock: this.updateVariantsField,
|
||||
}[key];
|
||||
},
|
||||
|
||||
updateVariants(field) {
|
||||
this.form.variants.forEach((variant) => {
|
||||
if (variant.is_selected) {
|
||||
this.callUpdateVariantsMethodByField(field.key)(
|
||||
variant,
|
||||
field,
|
||||
this.bulkEditVariants
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
bulkUpdateVariants() {
|
||||
if (
|
||||
!this.hasBulkEditVariantsUid &&
|
||||
!this.hasBulkEditVariantsField
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const field = {
|
||||
key: this.bulkEditVariantsField,
|
||||
value: this.bulkEditVariants[this.bulkEditVariantsField],
|
||||
};
|
||||
|
||||
this.updateVariants(field);
|
||||
this.resetBulkEditVariantFields();
|
||||
|
||||
toaster(trans("product::products.variants.bulk_variants_updated"), {
|
||||
type: "default",
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
export default {
|
||||
methods: {
|
||||
transformMedia(formData) {
|
||||
formData.media = formData.media.map((data) => data.id);
|
||||
},
|
||||
|
||||
transformAttributes(formData) {
|
||||
formData.attributes = formData.attributes
|
||||
.filter(({ attribute_id }) => attribute_id !== "")
|
||||
.reduce((accumulator, { attribute_id, uid, values }) => {
|
||||
return { ...accumulator, [uid]: { attribute_id, values } };
|
||||
}, {});
|
||||
},
|
||||
|
||||
transformDownloads(formData) {
|
||||
formData.downloads = formData.downloads
|
||||
.filter(({ id }) => id !== null)
|
||||
.map(({ id }) => id);
|
||||
},
|
||||
|
||||
transformVariations(formData) {
|
||||
const PATHS = {
|
||||
text: ["id", "uid", "label"],
|
||||
color: ["id", "uid", "label", "color"],
|
||||
image: ["id", "uid", "label", "image"],
|
||||
};
|
||||
|
||||
formData.variations = formData.variations
|
||||
.filter(({ name, type }) => Boolean(name) || type !== "")
|
||||
.reduce((accumulator, variation) => {
|
||||
if (variation.type === "") {
|
||||
variation.values = [];
|
||||
} else {
|
||||
variation.values = variation.values.reduce(
|
||||
(valueAccumulator, value) => {
|
||||
value = _.pick(value, PATHS[variation.type]);
|
||||
|
||||
if (variation.type === "image") {
|
||||
value.image = value.image.id;
|
||||
}
|
||||
|
||||
return {
|
||||
...valueAccumulator,
|
||||
[value.uid]: value,
|
||||
};
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
return { ...accumulator, [variation.uid]: variation };
|
||||
}, {});
|
||||
},
|
||||
|
||||
transformVariants(formData) {
|
||||
formData.variants = formData.variants.reduce(
|
||||
(accumulator, variant) => {
|
||||
variant.media = variant.media.map(({ id }) => id);
|
||||
|
||||
return { ...accumulator, [variant.uid]: variant };
|
||||
},
|
||||
{}
|
||||
);
|
||||
},
|
||||
|
||||
transformOptions(formData) {
|
||||
const PATHS = {
|
||||
text: ["id", "uid", "price", "price_type"],
|
||||
select: ["id", "uid", "label", "price", "price_type"],
|
||||
};
|
||||
|
||||
formData.options = formData.options
|
||||
.filter(({ name, type }) => Boolean(name) || type !== "")
|
||||
.reduce((accumulator, option) => {
|
||||
if (option.type === "") {
|
||||
option.values = [];
|
||||
} else {
|
||||
option.values = option.values.reduce(
|
||||
(valueAccumulator, value) => {
|
||||
const optionType = this.isOptionTypeText(option)
|
||||
? "text"
|
||||
: "select";
|
||||
|
||||
value = _.pick(value, PATHS[optionType]);
|
||||
|
||||
return {
|
||||
...valueAccumulator,
|
||||
[value.uid]: value,
|
||||
};
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
return { ...accumulator, [option.uid]: option };
|
||||
}, {});
|
||||
},
|
||||
|
||||
transformData(data) {
|
||||
const formData = JSON.parse(JSON.stringify(data));
|
||||
|
||||
this.transformMedia(formData);
|
||||
this.transformAttributes(formData);
|
||||
this.transformDownloads(formData);
|
||||
this.transformVariations(formData);
|
||||
this.transformVariants(formData);
|
||||
this.transformOptions(formData);
|
||||
|
||||
return formData;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
export default {
|
||||
watch: {
|
||||
"form.downloads": {
|
||||
immediate: true,
|
||||
handler(newValue) {
|
||||
if (newValue.length === 0) {
|
||||
this.addDownload({ preventMediaPicker: true });
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
addDownload({ preventMediaPicker }) {
|
||||
const downloads = this.form.downloads;
|
||||
|
||||
downloads.push({
|
||||
id: null,
|
||||
filename: null,
|
||||
});
|
||||
|
||||
if (!preventMediaPicker) {
|
||||
this.chooseDownloadableFile(downloads.length - 1);
|
||||
}
|
||||
},
|
||||
|
||||
deleteDownload(index) {
|
||||
this.form.downloads.splice(index, 1);
|
||||
},
|
||||
|
||||
chooseDownloadableFile(index) {
|
||||
let picker = new MediaPicker();
|
||||
|
||||
picker.on("select", ({ id, filename }) => {
|
||||
this.form.downloads.splice(index, 1, {
|
||||
id,
|
||||
filename,
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isLeftColumnSectionDragging: false,
|
||||
isRightColumnSectionDragging: false,
|
||||
formLeftSections: [],
|
||||
formRightSections: [],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
storeFormSections() {
|
||||
return {
|
||||
get: (sortable) => {
|
||||
return this.getSectionOrder(sortable.options.dataName);
|
||||
},
|
||||
set: (sortable) => {
|
||||
return this.setSectionOrder(sortable);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.formLeftSections = this.getSectionOrder(
|
||||
"product-form-left-sections"
|
||||
);
|
||||
this.formRightSections = this.getSectionOrder(
|
||||
"product-form-right-sections"
|
||||
);
|
||||
},
|
||||
|
||||
methods: {
|
||||
getDefaultSectionOrder(key) {
|
||||
return {
|
||||
"product-form-left-sections": [
|
||||
"attributes",
|
||||
"downloads",
|
||||
"variations",
|
||||
"variants",
|
||||
"options",
|
||||
],
|
||||
"product-form-right-sections": [
|
||||
"price",
|
||||
"inventory",
|
||||
"media",
|
||||
"linked_products",
|
||||
"seo",
|
||||
"additional",
|
||||
],
|
||||
}[key];
|
||||
},
|
||||
|
||||
getSectionOrder(key) {
|
||||
const sectionsOrder = JSON.parse(localStorage.getItem(key));
|
||||
|
||||
return sectionsOrder === null
|
||||
? this.getDefaultSectionOrder(key)
|
||||
: sectionsOrder;
|
||||
},
|
||||
|
||||
setSectionOrder(sortable) {
|
||||
this.$nextTick(() => {
|
||||
localStorage.setItem(
|
||||
sortable.options.dataName,
|
||||
JSON.stringify(sortable.toArray())
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
notifySectionOrderChange() {
|
||||
toaster(trans("product::products.section.order_saved"), {
|
||||
type: "default",
|
||||
});
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.initColorPicker();
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
198
Modules/Product/Resources/assets/admin/js/mixins/OptionMixin.js
Normal file
198
Modules/Product/Resources/assets/admin/js/mixins/OptionMixin.js
Normal file
@@ -0,0 +1,198 @@
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
globalOptionId: "",
|
||||
addingGlobalOption: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
isAddGlobalOptionDisabled() {
|
||||
return this.globalOptionId === "" || this.addingGlobalOption;
|
||||
},
|
||||
|
||||
isCollapsedOptionsAccordion() {
|
||||
return this.form.options.every(({ is_open }) => is_open === false);
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
"form.options": {
|
||||
immediate: true,
|
||||
handler(newValue) {
|
||||
if (newValue.length === 0) {
|
||||
this.addOption({ preventFocus: true });
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
prepareOptions(options) {
|
||||
options.forEach((option) => {
|
||||
this.$set(option, "uid", this.uid());
|
||||
this.$set(option, "is_open", false);
|
||||
|
||||
option.values.forEach((_, valueIndex) => {
|
||||
this.$set(option.values[valueIndex], "uid", this.uid());
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
addOption({ preventFocus }) {
|
||||
const options = this.form.options;
|
||||
const uid = this.uid();
|
||||
|
||||
options.push({
|
||||
uid,
|
||||
type: "",
|
||||
is_open: true,
|
||||
is_global: false,
|
||||
is_required: false,
|
||||
values: [
|
||||
{
|
||||
uid: this.uid(),
|
||||
price_type: "fixed",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!preventFocus) {
|
||||
this.focusField({
|
||||
selector: `#options-${uid}-name`,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
deleteOption(index, uid) {
|
||||
this.form.options.splice(index, 1);
|
||||
this.clearErrors({ name: "options", uid });
|
||||
},
|
||||
|
||||
changeOptionType(index, uid) {
|
||||
let option = this.form.options[index];
|
||||
const fieldName = this.isOptionTypeText(option) ? "price" : "label";
|
||||
|
||||
this.clearValuesError({ name: "options", uid });
|
||||
|
||||
if (option.values.length === 1) {
|
||||
this.focusField({
|
||||
selector: `#options-${uid}-values-${option.values[0].uid}-${fieldName}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
isOptionTypeText(option) {
|
||||
const TYPES = ["field", "textarea", "date", "date_time", "time"];
|
||||
|
||||
if (TYPES.includes(option.type)) {
|
||||
option.values.length > 1 && option.values.splice(1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
isOptionTypeSelect({ type }) {
|
||||
const TYPES = [
|
||||
"dropdown",
|
||||
"checkbox",
|
||||
"checkbox_custom",
|
||||
"radio",
|
||||
"radio_custom",
|
||||
"multiple_select",
|
||||
];
|
||||
|
||||
return TYPES.includes(type) ? true : false;
|
||||
},
|
||||
|
||||
addOptionRow(index, optionUid) {
|
||||
const valueUid = this.uid();
|
||||
|
||||
this.form.options[index].values.push({
|
||||
uid: valueUid,
|
||||
price_type: "fixed",
|
||||
});
|
||||
|
||||
this.focusField({
|
||||
selector: `#options-${optionUid}-values-${valueUid}-label`,
|
||||
});
|
||||
},
|
||||
|
||||
addOptionRowOnPressEnter(event, optionIndex, valueIndex) {
|
||||
const option = this.form.options[optionIndex];
|
||||
const values = option.values;
|
||||
|
||||
if (event.target.value === "") return;
|
||||
|
||||
if (values.length - 1 === valueIndex) {
|
||||
this.addOptionRow(optionIndex, option.uid);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (valueIndex < values.length - 1) {
|
||||
$(
|
||||
`#options-${option.uid}-values-${
|
||||
values[valueIndex + 1].uid
|
||||
}-label`
|
||||
).trigger("focus");
|
||||
}
|
||||
},
|
||||
|
||||
deleteOptionRow(optionIndex, optionUid, valueIndex, valueUid) {
|
||||
const option = this.form.options[optionIndex];
|
||||
|
||||
option.values.splice(valueIndex, 1);
|
||||
|
||||
this.clearValueRowErrors({ name: "options", optionUid, valueUid });
|
||||
|
||||
if (option.values.length === 0) {
|
||||
this.addOptionRow(optionIndex, optionUid);
|
||||
}
|
||||
},
|
||||
|
||||
addGlobalOption() {
|
||||
if (this.globalOptionId === "") return;
|
||||
|
||||
this.addingGlobalOption = true;
|
||||
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: route("admin.options.show", this.globalOptionId),
|
||||
dataType: "json",
|
||||
success: (option) => {
|
||||
option.uid = this.uid();
|
||||
option.is_open = true;
|
||||
|
||||
option.values.forEach((value) => {
|
||||
value.uid = this.uid();
|
||||
});
|
||||
|
||||
this.form.options.push(option);
|
||||
|
||||
this.$nextTick(() => {
|
||||
$(`#options-${option.uid}-name`).trigger("focus");
|
||||
});
|
||||
|
||||
toaster(
|
||||
trans("product::products.options.option_inserted"),
|
||||
{
|
||||
type: "default",
|
||||
}
|
||||
);
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
toaster(error.responseJSON.message, {
|
||||
type: "error",
|
||||
});
|
||||
})
|
||||
.always(() => {
|
||||
this.globalOptionId = "";
|
||||
this.addingGlobalOption = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
243
Modules/Product/Resources/assets/admin/js/mixins/ProductMixin.js
Normal file
243
Modules/Product/Resources/assets/admin/js/mixins/ProductMixin.js
Normal file
@@ -0,0 +1,243 @@
|
||||
import Admin from "@admin/js/Admin";
|
||||
import md5 from "blueimp-md5";
|
||||
import tinyMCE from "tinymce";
|
||||
import draggable from "vuedraggable";
|
||||
import Selectize from "vue2-selectize";
|
||||
import flatPickr from "vue-flatpickr-component";
|
||||
import FormSectionMixin from "./FormSectionMixin";
|
||||
import AttributeMixin from "./AttributeMixin";
|
||||
import VariationMixin from "./VariationMixin";
|
||||
import VariantMixin from "./VariantMixin";
|
||||
import BulkEditVariantsMixin from "./BulkEditVariantsMixin";
|
||||
import OptionMixin from "./OptionMixin";
|
||||
import DownloadMixin from "./DownloadMixin";
|
||||
import DataTransformMixin from "./DataTransformMixin";
|
||||
import { toaster } from "@admin/js/Toaster";
|
||||
|
||||
window.md5 = md5;
|
||||
window.toaster = toaster;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
draggable,
|
||||
Selectize,
|
||||
flatPickr,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
FormSectionMixin,
|
||||
AttributeMixin,
|
||||
VariationMixin,
|
||||
VariantMixin,
|
||||
BulkEditVariantsMixin,
|
||||
OptionMixin,
|
||||
DownloadMixin,
|
||||
DataTransformMixin,
|
||||
],
|
||||
|
||||
mounted() {
|
||||
this.initTinyMCE();
|
||||
|
||||
window.admin = new Admin();
|
||||
},
|
||||
|
||||
methods: {
|
||||
uid() {
|
||||
return Math.random().toString(36).slice(3);
|
||||
},
|
||||
|
||||
focusEditor() {
|
||||
tinyMCE.get("description").focus();
|
||||
},
|
||||
|
||||
focusField({ selector, key }) {
|
||||
if (key !== undefined) {
|
||||
this.errors.clear(key);
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
$(`${selector}`).trigger("focus");
|
||||
});
|
||||
},
|
||||
|
||||
regenerateVariationsAndVariantsUid() {
|
||||
this.regenerateVariationsUid();
|
||||
|
||||
const variations = this.getFilteredVariations();
|
||||
const newVariants = this.generateNewVariants(variations);
|
||||
|
||||
newVariants.forEach(({ uids }, index) => {
|
||||
this.$set(this.form.variants[index], "uid", md5(uids));
|
||||
this.$set(this.form.variants[index], "uids", uids);
|
||||
});
|
||||
},
|
||||
|
||||
hasAnyError({ name, uid }) {
|
||||
return Object.keys(this.errors.errors).some((key) =>
|
||||
key.startsWith(`${name}.${uid}`)
|
||||
);
|
||||
},
|
||||
|
||||
clearErrors({ name, uid }) {
|
||||
this.clearMatchedErrors(`${name}.${uid}`);
|
||||
},
|
||||
|
||||
clearValuesError({ name, uid }) {
|
||||
this.clearMatchedErrors(`${name}.${uid}.values`);
|
||||
},
|
||||
|
||||
clearValueRowErrors({ name, uid, valueUid }) {
|
||||
this.clearMatchedErrors(`${name}.${uid}.values.${valueUid}`);
|
||||
},
|
||||
|
||||
clearMatchedErrors(str) {
|
||||
Object.keys(this.errors.errors).forEach((key) => {
|
||||
if (key.startsWith(str)) {
|
||||
this.errors.clear(key);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
scrollToFirstErrorField(elements) {
|
||||
this.$nextTick(() => {
|
||||
[...elements]
|
||||
.find(
|
||||
(el) => el.name === Object.keys(this.errors.errors)[0]
|
||||
)
|
||||
.focus();
|
||||
});
|
||||
},
|
||||
|
||||
addMedia() {
|
||||
const picker = new MediaPicker({ type: "image", multiple: true });
|
||||
|
||||
picker.on("select", ({ id, path }) => {
|
||||
this.form.media.push({
|
||||
id: +id,
|
||||
path,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
removeMedia(index) {
|
||||
this.form.media.splice(index, 1);
|
||||
},
|
||||
|
||||
preventLastSlideDrag(event) {
|
||||
return event.related.className.indexOf("disabled") === -1;
|
||||
},
|
||||
|
||||
toggleAccordions({ selector, state, data }) {
|
||||
const event = new Event("click");
|
||||
const elements = document.querySelectorAll(selector);
|
||||
|
||||
if (!state) {
|
||||
data.forEach(({ is_open }, index) => {
|
||||
if (is_open) {
|
||||
elements[index].dispatchEvent(event);
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
[...elements].forEach((element) => {
|
||||
element.dispatchEvent(event);
|
||||
});
|
||||
},
|
||||
|
||||
toggleAccordion(event, data) {
|
||||
const target = $(event.currentTarget);
|
||||
const panelTitle = target.find('[data-toggle="collapse"]');
|
||||
const panelBody = target.next();
|
||||
|
||||
if (data.is_open) {
|
||||
panelBody.css("display", "block");
|
||||
}
|
||||
|
||||
panelTitle.attr("data-transition", true);
|
||||
|
||||
this.$set(data, "is_open", !data.is_open);
|
||||
|
||||
panelBody.slideToggle(300, () => {
|
||||
panelTitle.attr("data-transition", false);
|
||||
panelBody.removeAttr("style");
|
||||
});
|
||||
},
|
||||
|
||||
setSearchableSelectizeConfig() {
|
||||
this.searchableSelectizeConfig = {
|
||||
valueField: "id",
|
||||
labelField: "name",
|
||||
searchField: "name",
|
||||
load: function (query, callback) {
|
||||
var url = route("admin.products.index");
|
||||
|
||||
if (url === undefined || query.length === 0) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
$.get(url + "?query=" + query, callback, "json");
|
||||
},
|
||||
plugins: ["remove_button"],
|
||||
};
|
||||
},
|
||||
|
||||
initTinyMCE() {
|
||||
tinyMCE.baseURL = `${FleetCart.baseUrl}/build/assets/tinymce`;
|
||||
|
||||
tinyMCE.init({
|
||||
selector: ".wysiwyg",
|
||||
theme: "silver",
|
||||
height: 350,
|
||||
menubar: false,
|
||||
branding: false,
|
||||
image_advtab: true,
|
||||
automatic_uploads: true,
|
||||
media_alt_source: false,
|
||||
media_poster: false,
|
||||
relative_urls: false,
|
||||
toolbar_mode: "sliding", // Possible values: floating, sliding, scrolling, wrap
|
||||
directionality: FleetCart.rtl ? "rtl" : "ltr",
|
||||
cache_suffix: `?v=${FleetCart.version}`,
|
||||
plugins:
|
||||
"lists, link, table, image, media, paste, autosave, autolink,quickbars, wordcount, code, fullscreen",
|
||||
toolbar:
|
||||
"styleselect | bold italic underline strikethrough blockquote | bullist numlist | alignleft aligncenter alignright alignjustify | outdent indent | forecolor removeformat | table | image media link | code fullscreen",
|
||||
quickbars_selection_toolbar:
|
||||
"bold italic | quicklink h2 h3 blockquote quickimage quicktable",
|
||||
setup: (editor) => {
|
||||
editor.on("change", () => {
|
||||
editor.save();
|
||||
editor.getElement().dispatchEvent(new Event("input"));
|
||||
|
||||
this.errors.clear("description");
|
||||
});
|
||||
},
|
||||
images_upload_handler: (blobInfo, success, failure) => {
|
||||
let formData = new FormData();
|
||||
|
||||
formData.append(
|
||||
"file",
|
||||
blobInfo.blob(),
|
||||
blobInfo.filename()
|
||||
);
|
||||
|
||||
$.ajax({
|
||||
method: "POST",
|
||||
url: route("admin.media.store"),
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
})
|
||||
.then((file) => {
|
||||
success(file.path);
|
||||
})
|
||||
.catch((xhr) => {
|
||||
failure(xhr.responseJSON.message);
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
400
Modules/Product/Resources/assets/admin/js/mixins/VariantMixin.js
Normal file
400
Modules/Product/Resources/assets/admin/js/mixins/VariantMixin.js
Normal file
@@ -0,0 +1,400 @@
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
defaultVariantUid: "",
|
||||
variantPosition: 0,
|
||||
variantsLength: 0,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
hasAnyVariant() {
|
||||
return this.form.variants.length !== 0;
|
||||
},
|
||||
|
||||
isCollapsedVariantsAccordion() {
|
||||
return this.form.variants.every(({ is_open }) => is_open === false);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
prepareVariants(variants) {
|
||||
variants.forEach((variant) => {
|
||||
this.$set(variant, "position", this.variantPosition++);
|
||||
this.$set(variant, "is_open", false);
|
||||
this.$set(variant, "is_selected", false);
|
||||
});
|
||||
},
|
||||
|
||||
changeDefaultVariant(uid) {
|
||||
const variants = this.form.variants;
|
||||
const index = variants.findIndex((variant) => variant.uid === uid);
|
||||
|
||||
if (variants[index].is_active === true) {
|
||||
this.resetDefaultVariant();
|
||||
this.defaultVariantUid = variants[index].uid;
|
||||
this.$set(variants[index], "is_default", true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.defaultVariantUid = this.defaultVariantUid;
|
||||
},
|
||||
|
||||
setDefaultVariant() {
|
||||
const variants = this.form.variants;
|
||||
const index = variants.findIndex(
|
||||
({ uid }) => uid === this.defaultVariantUid
|
||||
);
|
||||
|
||||
this.resetDefaultVariant();
|
||||
|
||||
const variant = variants[index === -1 ? 0 : index];
|
||||
|
||||
this.defaultVariantUid = variant.uid;
|
||||
this.$set(variant, "is_default", true);
|
||||
|
||||
if (index === -1) {
|
||||
this.defaultVariantUid = variants[0].uid;
|
||||
this.$set(variants[0], "is_active", true);
|
||||
}
|
||||
},
|
||||
|
||||
isActiveVariant(index) {
|
||||
return this.form.variants[index].is_active;
|
||||
},
|
||||
|
||||
changeVariantStatus(variantUid) {
|
||||
if (this.defaultVariantUid === variantUid) {
|
||||
toaster(
|
||||
trans("product::products.variants.disable_default_variant"),
|
||||
{
|
||||
type: "default",
|
||||
}
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearErrors({
|
||||
name: "variants",
|
||||
uid: variantUid,
|
||||
});
|
||||
},
|
||||
|
||||
resetDefaultVariant() {
|
||||
this.form.variants.some((variant) => {
|
||||
if (variant.is_default === true) {
|
||||
this.$set(variant, "is_default", false);
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getFilteredVariations() {
|
||||
return this.form.variations
|
||||
.map(({ type, values }) =>
|
||||
values
|
||||
.map(({ uid, label }) => {
|
||||
if (type !== "" && Boolean(label)) {
|
||||
return { uid, label };
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
)
|
||||
.filter((data) => data.length !== 0);
|
||||
},
|
||||
|
||||
generateNewVariants(variations) {
|
||||
return variations
|
||||
.reduce((accumulator, currentValue) =>
|
||||
accumulator.flatMap((x) =>
|
||||
currentValue.map((y) => {
|
||||
return {
|
||||
uid: x.uid + "." + y.uid,
|
||||
label: x.label + " / " + y.label,
|
||||
};
|
||||
})
|
||||
)
|
||||
)
|
||||
.map(({ uid, label }) => {
|
||||
return {
|
||||
uids: uid.split(".").sort().join("."),
|
||||
name: label,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
generateVariants() {
|
||||
this.$nextTick(() => {
|
||||
this.initColorPicker();
|
||||
this.updateColorThumbnails();
|
||||
});
|
||||
|
||||
// Filter empty variation values
|
||||
const variations = this.getFilteredVariations();
|
||||
|
||||
if (variations.length === 0) {
|
||||
this.form.variants = [];
|
||||
this.variantsLength = 0;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const newVariants = this.generateNewVariants(variations);
|
||||
const oldVariants = this.form.variants.map(({ uids }) => {
|
||||
return {
|
||||
uids,
|
||||
};
|
||||
});
|
||||
|
||||
if (newVariants.length > this.variantsLength) {
|
||||
// Variation added
|
||||
this.addVariants(newVariants, oldVariants);
|
||||
} else if (newVariants.length < this.variantsLength) {
|
||||
// Variation removed
|
||||
this.removeVariants(newVariants, oldVariants);
|
||||
} else if (newVariants.length === this.variantsLength) {
|
||||
// Variations reordered
|
||||
this.reorderVariants(newVariants, oldVariants);
|
||||
}
|
||||
|
||||
this.variantsLength = newVariants.length;
|
||||
this.setDefaultVariant();
|
||||
},
|
||||
|
||||
addVariants(newVariants, oldVariants) {
|
||||
this.notifyVariantsCreated(newVariants.length);
|
||||
|
||||
// Add initial variation with single or multiple values when variants are empty
|
||||
if (oldVariants.length === 0) {
|
||||
newVariants.forEach((newVariant) => {
|
||||
this.form.variants.push(
|
||||
this.variantDefaultData(newVariant)
|
||||
);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// A new single value has been added with existing variation values
|
||||
if (this.hasCommonVariantUids(newVariants, oldVariants)) {
|
||||
const oldVariantsUids = oldVariants.map(({ uids }) => uids);
|
||||
|
||||
newVariants.forEach((newVariant, index) => {
|
||||
if (!oldVariantsUids.includes(newVariant.uids)) {
|
||||
this.form.variants.splice(
|
||||
index,
|
||||
0,
|
||||
this.variantDefaultData(newVariant)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// A new variation with multiple values has been added
|
||||
const matchedUids = [];
|
||||
|
||||
oldVariants.forEach(({ uids }) => {
|
||||
newVariants.forEach((newVariant, index) => {
|
||||
const doesUidExist = uids
|
||||
.split(".")
|
||||
.every((uids) =>
|
||||
newVariant.uids.split(".").includes(uids)
|
||||
);
|
||||
|
||||
if (doesUidExist && !matchedUids.includes(uids)) {
|
||||
matchedUids.push(uids);
|
||||
this.setVariantData(newVariant, index);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (doesUidExist) {
|
||||
this.form.variants.splice(
|
||||
index,
|
||||
0,
|
||||
this.variantDefaultData(newVariant)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
removeVariants(newVariants, oldVariants) {
|
||||
this.resetBulkEditVariantFields();
|
||||
this.notifyVariantsRemoved(oldVariants.length - newVariants.length);
|
||||
|
||||
// Variation single value has been removed
|
||||
if (this.hasCommonVariantUids(newVariants, oldVariants)) {
|
||||
const newVariantsUids = newVariants.map(({ uids }) => uids);
|
||||
|
||||
oldVariants.forEach(({ uids }) => {
|
||||
if (!newVariantsUids.includes(uids)) {
|
||||
const index = this.form.variants.findIndex(
|
||||
(variant) => variant.uids === uids
|
||||
);
|
||||
|
||||
this.clearErrors({
|
||||
name: "variants",
|
||||
uid: this.form.variants[index].uid,
|
||||
});
|
||||
this.form.variants.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// A variation with multiple values has been removed
|
||||
const matchedUids = [];
|
||||
|
||||
newVariants.forEach(({ uids, name }) => {
|
||||
oldVariants.forEach((oldVariant) => {
|
||||
const index = this.form.variants.findIndex(
|
||||
(variant) => variant.uids === oldVariant.uids
|
||||
);
|
||||
const doesUidExist = uids
|
||||
.split(".")
|
||||
.every((uids) =>
|
||||
oldVariant.uids.split(".").includes(uids)
|
||||
);
|
||||
|
||||
if (doesUidExist && !matchedUids.includes(uids)) {
|
||||
matchedUids.push(uids);
|
||||
this.setVariantData({ uids, name }, index);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (doesUidExist) {
|
||||
this.clearErrors({
|
||||
name: "variants",
|
||||
uid: this.form.variants[index].uid,
|
||||
});
|
||||
this.form.variants.splice(index, 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
reorderVariants(newVariants, oldVariants) {
|
||||
// Reordered variations or variation values
|
||||
const newVariantUids = newVariants.map(({ uids }) => uids);
|
||||
|
||||
if (this.hasCommonVariantUids(newVariants, oldVariants)) {
|
||||
oldVariants.forEach(({ uids }) => {
|
||||
const index = newVariantUids.indexOf(uids);
|
||||
const formIndex = this.form.variants.findIndex(
|
||||
(variant) => variant.uids === uids
|
||||
);
|
||||
|
||||
// Update variant data before swap
|
||||
this.setVariantData(
|
||||
{ name: newVariants[index].name },
|
||||
formIndex
|
||||
);
|
||||
|
||||
// Swap variant elements
|
||||
this.form.variants[formIndex] = this.form.variants.splice(
|
||||
index,
|
||||
1,
|
||||
this.form.variants[formIndex]
|
||||
)[0];
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// A new variation with a single value has been added
|
||||
newVariants.forEach((newVariant, index) => {
|
||||
this.setVariantData(newVariant, index);
|
||||
});
|
||||
},
|
||||
|
||||
hasCommonVariantUids(newVariants, oldVariants) {
|
||||
// Check if the old variants UID is present in the new variants
|
||||
return oldVariants.some(({ uids }) =>
|
||||
newVariants.map(({ uids }) => uids).includes(uids)
|
||||
);
|
||||
},
|
||||
|
||||
setVariantData({ uids, name }, index) {
|
||||
if (uids !== undefined) {
|
||||
this.$set(this.form.variants[index], "uid", md5(uids));
|
||||
this.$set(this.form.variants[index], "uids", uids);
|
||||
}
|
||||
|
||||
this.$set(this.form.variants[index], "name", name);
|
||||
},
|
||||
|
||||
variantDefaultData({ uids, name }) {
|
||||
return {
|
||||
position: this.variantPosition++,
|
||||
uid: md5(uids),
|
||||
uids,
|
||||
name,
|
||||
media: [],
|
||||
is_active: true,
|
||||
is_open: false,
|
||||
is_default: false,
|
||||
is_selected: false,
|
||||
special_price_type: "fixed",
|
||||
manage_stock: 0,
|
||||
in_stock: 1,
|
||||
};
|
||||
},
|
||||
|
||||
resetVariants() {
|
||||
this.form.variants = [];
|
||||
},
|
||||
|
||||
addVariantMedia(index) {
|
||||
const picker = new MediaPicker({ type: "image", multiple: true });
|
||||
|
||||
picker.on("select", ({ id, path }) => {
|
||||
this.form.variants[index].media.push({
|
||||
id: +id,
|
||||
path,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
removeVariantMedia(variantIndex, mediaIndex) {
|
||||
this.form.variants[variantIndex].media.splice(mediaIndex, 1);
|
||||
},
|
||||
|
||||
notifyVariantChanges({ count, status }) {
|
||||
toaster(
|
||||
trans(`product::products.variants.variants_${status}`, {
|
||||
count,
|
||||
suffix: trans(
|
||||
`product::products.variants.${
|
||||
count > 1 ? "variants" : "variant"
|
||||
}`
|
||||
),
|
||||
}).toLowerCase(),
|
||||
{
|
||||
type: "default",
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
notifyVariantsCreated(count) {
|
||||
this.notifyVariantChanges({ count, status: "created" });
|
||||
},
|
||||
|
||||
notifyVariantsRemoved(count) {
|
||||
this.notifyVariantChanges({ count, status: "removed" });
|
||||
},
|
||||
|
||||
notifyVariantsReordered() {
|
||||
toaster(trans("product::products.variants.variants_reordered"), {
|
||||
type: "default",
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,316 @@
|
||||
import Coloris from "@melloware/coloris";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
globalVariationId: "",
|
||||
addingGlobalVariation: false,
|
||||
variations: [],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
isAddGlobalVariationDisabled() {
|
||||
return this.globalVariationId === "" || this.addingGlobalVariation;
|
||||
},
|
||||
|
||||
isCollapsedVariationsAccordion() {
|
||||
return this.form.variations.every(
|
||||
({ is_open }) => is_open === false
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
"form.variations": {
|
||||
immediate: true,
|
||||
handler(newValue) {
|
||||
if (newValue.length === 0) {
|
||||
this.addVariation({ preventFocus: true });
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.initColorPicker();
|
||||
this.hideColorPicker();
|
||||
},
|
||||
|
||||
methods: {
|
||||
prepareVariations(variations) {
|
||||
variations.forEach((variation) => {
|
||||
this.$set(variation, "is_open", false);
|
||||
});
|
||||
},
|
||||
|
||||
regenerateVariationsUid() {
|
||||
this.form.variations.forEach((variation) => {
|
||||
this.$set(variation, "uid", this.uid());
|
||||
|
||||
variation.values.forEach((_, valueIndex) => {
|
||||
this.$set(variation.values[valueIndex], "uid", this.uid());
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
reorderVariations() {
|
||||
this.generateVariants();
|
||||
this.notifyVariantsReordered();
|
||||
},
|
||||
|
||||
reorderVariationValues() {
|
||||
this.generateVariants();
|
||||
this.notifyVariantsReordered();
|
||||
},
|
||||
|
||||
addVariation({ preventFocus }) {
|
||||
const uid = this.uid();
|
||||
|
||||
this.form.variations.push({
|
||||
uid,
|
||||
type: "",
|
||||
is_global: false,
|
||||
is_open: true,
|
||||
values: [
|
||||
{
|
||||
uid: this.uid(),
|
||||
image: {
|
||||
id: null,
|
||||
path: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!preventFocus) {
|
||||
console.log("add");
|
||||
this.focusField({
|
||||
selector: `#variations-${uid}-name`,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
deleteVariation(index, uid) {
|
||||
this.form.variations.splice(index, 1);
|
||||
this.clearErrors({ name: "variations", uid });
|
||||
this.generateVariants();
|
||||
},
|
||||
|
||||
changeVariationType(value, index, uid) {
|
||||
const variation = this.form.variations[index];
|
||||
|
||||
if (value !== "" && variation.values.length === 1) {
|
||||
this.focusField({
|
||||
selector: `#variations-${uid}-values-${variation.values[0].uid}-label`,
|
||||
});
|
||||
}
|
||||
|
||||
if (value === "text") {
|
||||
variation.values.forEach((value) => {
|
||||
this.errors.clear([
|
||||
`variations.${uid}.values.${value.uid}.color`,
|
||||
`variations.${uid}.values.${value.uid}.image`,
|
||||
]);
|
||||
});
|
||||
} else if (value === "color") {
|
||||
variation.values.forEach((value) => {
|
||||
this.errors.clear(
|
||||
`variations.${uid}.values.${value.uid}.image`
|
||||
);
|
||||
});
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.initColorPicker();
|
||||
});
|
||||
} else if (value === "image") {
|
||||
variation.values.forEach((value, valueIndex) => {
|
||||
if (!value.image) {
|
||||
this.$set(variation.values[valueIndex], "image", {
|
||||
id: null,
|
||||
path: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
variation.values.forEach((value) => {
|
||||
this.errors.clear(
|
||||
`variations.${uid}.values.${value.uid}.color`
|
||||
);
|
||||
});
|
||||
} else {
|
||||
this.clearValuesError(index, uid);
|
||||
}
|
||||
},
|
||||
|
||||
chooseVariationImage(
|
||||
variationIndex,
|
||||
variationUid,
|
||||
valueIndex,
|
||||
valueUid
|
||||
) {
|
||||
let picker = new MediaPicker({ type: "image" });
|
||||
|
||||
picker.on("select", ({ id, path }) => {
|
||||
this.form.variations[variationIndex].values[valueIndex].image =
|
||||
{
|
||||
id: +id,
|
||||
path,
|
||||
};
|
||||
|
||||
this.errors.clear(
|
||||
`variations.${variationUid}.values.${valueUid}.image`
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
addVariationRow(index, variationUid) {
|
||||
const valueUid = this.uid();
|
||||
|
||||
this.form.variations[index].values.push({
|
||||
uid: valueUid,
|
||||
image: {
|
||||
id: null,
|
||||
path: null,
|
||||
},
|
||||
});
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.initColorPicker();
|
||||
|
||||
$(
|
||||
`#variations-${variationUid}-values-${valueUid}-label`
|
||||
).trigger("focus");
|
||||
});
|
||||
},
|
||||
|
||||
addVariationRowOnPressEnter(event, variationIndex, valueIndex) {
|
||||
const variation = this.form.variations[variationIndex];
|
||||
const values = variation.values;
|
||||
|
||||
if (event.target.value === "") return;
|
||||
|
||||
if (values.length - 1 === valueIndex) {
|
||||
this.addVariationRow(variationIndex, variation.uid);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (valueIndex < values.length - 1) {
|
||||
$(
|
||||
`#variations-${variation.uid}-values-${
|
||||
values[valueIndex + 1].uid
|
||||
}-label`
|
||||
).trigger("focus");
|
||||
}
|
||||
},
|
||||
|
||||
deleteVariationRow(variationIndex, variationUid, valueIndex, valueUid) {
|
||||
const variation = this.form.variations[variationIndex];
|
||||
|
||||
variation.values.splice(valueIndex, 1);
|
||||
|
||||
this.clearValueRowErrors({
|
||||
name: "variations",
|
||||
variationUid,
|
||||
valueUid,
|
||||
});
|
||||
|
||||
if (variation.values.length === 0) {
|
||||
this.addVariationRow(variationIndex, variationUid);
|
||||
}
|
||||
|
||||
this.generateVariants();
|
||||
},
|
||||
|
||||
updateColorThumbnails() {
|
||||
this.form.variations.forEach(({ uid, type, values }) => {
|
||||
if (type !== "color") return;
|
||||
|
||||
const elements = document.querySelectorAll(
|
||||
`.variation-${uid} .clr-field`
|
||||
);
|
||||
|
||||
values.forEach(({ color }, valueIndex) => {
|
||||
elements[valueIndex].style.color = color || "";
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
initColorPicker() {
|
||||
Coloris.init();
|
||||
|
||||
Coloris({
|
||||
el: ".color-picker",
|
||||
alpha: false,
|
||||
rtl: FleetCart.rtl,
|
||||
theme: "large",
|
||||
wrap: true,
|
||||
format: "hex",
|
||||
selectInput: true,
|
||||
swatches: [
|
||||
"#D01C1F",
|
||||
"#3AA845",
|
||||
"#118257",
|
||||
"#0A33AE",
|
||||
"#0D46A0",
|
||||
"#000000",
|
||||
"#5F4C3A",
|
||||
"#726E6E",
|
||||
"#F6D100",
|
||||
"#C0E506",
|
||||
"#FF540A",
|
||||
"#C5A996",
|
||||
"#4B80BE",
|
||||
"#A1C3DA",
|
||||
"#C8BFC2",
|
||||
"#A9A270",
|
||||
],
|
||||
});
|
||||
},
|
||||
|
||||
hideColorPicker() {
|
||||
$(document).on("click", "#clr-swatches button", (e) => {
|
||||
$(e.currentTarget)
|
||||
.parents("#clr-picker")
|
||||
.removeClass("clr-open");
|
||||
});
|
||||
},
|
||||
|
||||
addGlobalVariation() {
|
||||
if (this.globalVariationId === "") return;
|
||||
|
||||
this.addingGlobalVariation = true;
|
||||
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: route("admin.variations.show", this.globalVariationId),
|
||||
dataType: "json",
|
||||
success: (variation) => {
|
||||
variation.uid = this.uid();
|
||||
variation.is_open = true;
|
||||
|
||||
variation.values.forEach((value) => {
|
||||
value.uid = this.uid();
|
||||
});
|
||||
|
||||
this.form.variations.push(variation);
|
||||
this.generateVariants();
|
||||
|
||||
this.$nextTick(() => {
|
||||
$(`#variations-${variation.uid}-name`).trigger("focus");
|
||||
});
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
toaster(error.responseJSON.message, {
|
||||
type: "error",
|
||||
});
|
||||
})
|
||||
.always(() => {
|
||||
this.globalVariationId = "";
|
||||
this.addingGlobalVariation = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,379 @@
|
||||
#images, #attachments {
|
||||
margin-bottom: 15px;
|
||||
@import "@admin/sass/wysiwyg";
|
||||
|
||||
.v-toast {
|
||||
padding: 20px 20px 60px;
|
||||
}
|
||||
|
||||
.product-form {
|
||||
.box {
|
||||
.box-header {
|
||||
.toggle-accordion {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin: 1px 10px 0 0;
|
||||
padding: 0 5px;
|
||||
color: #333333;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&.collapsed {
|
||||
i {
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
transition: 150ms ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.box-body {
|
||||
> .form-group {
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.accordion-box-content {
|
||||
.panel-heading {
|
||||
[data-transition="false"] {
|
||||
&:after {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-box-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.insert-template {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
select {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-media-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 20%);
|
||||
grid-template-rows: 1fr 1fr;
|
||||
margin: -6px;
|
||||
}
|
||||
|
||||
.media-grid-item {
|
||||
margin: 6px;
|
||||
border: 1px solid #d2d6de;
|
||||
border-radius: 3px;
|
||||
|
||||
&:first-child {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 3;
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 3;
|
||||
}
|
||||
|
||||
&.handle {
|
||||
.image-holder {
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
|
||||
&.media-picker {
|
||||
border-width: 2px;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.image-holder {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding-bottom: 90%;
|
||||
border: none;
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
|
||||
.variants {
|
||||
position: relative;
|
||||
|
||||
.fade {
|
||||
&-enter-active,
|
||||
&-leave-active {
|
||||
position: absolute;
|
||||
transition: 150ms ease-in-out;
|
||||
}
|
||||
|
||||
&-enter,
|
||||
&-leave-to {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bulk-edit-variants {
|
||||
.product-media-grid {
|
||||
margin-bottom: -6px;
|
||||
}
|
||||
}
|
||||
|
||||
.variants-group {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.panel-group {
|
||||
width: 100%;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
[data-toggle="collapse"] {
|
||||
padding: 12px 15px 13px;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
padding-top: 0;
|
||||
|
||||
label {
|
||||
min-height: 18px;
|
||||
}
|
||||
|
||||
[type="checkbox"]:checked + label:before,
|
||||
[type="checkbox"]:not(:checked) + label:before {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
[type="checkbox"]:checked + label:after,
|
||||
[type="checkbox"]:not(:checked) + label:after {
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
.switch {
|
||||
display: inline-flex;
|
||||
margin-right: 25px;
|
||||
padding-top: 0;
|
||||
|
||||
[type="checkbox"] {
|
||||
&:checked + label::after {
|
||||
left: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
padding-left: 30px;
|
||||
|
||||
&:before {
|
||||
top: 5px;
|
||||
height: 10px;
|
||||
width: 26px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
> .row {
|
||||
> .col-sm-4 {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
> .col-sm-8 {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.variant-fields {
|
||||
margin-bottom: -10px;
|
||||
overflow: hidden;
|
||||
|
||||
> .row {
|
||||
> .col-sm-6 {
|
||||
&:nth-child(1) {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.variant-name {
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.variant-badge {
|
||||
margin-left: 5px;
|
||||
|
||||
li {
|
||||
display: inline-flex;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 11px;
|
||||
padding: 5px 8px;
|
||||
border-radius: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.variant {
|
||||
&-enter-active,
|
||||
&-leave-active,
|
||||
&-move {
|
||||
transition: 150ms ease-in-out;
|
||||
}
|
||||
|
||||
&-enter,
|
||||
&-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
&-enter-from {
|
||||
opacity: 0;
|
||||
scale: 0;
|
||||
}
|
||||
|
||||
&-enter-to {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
|
||||
&-leave-from {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.product-media-grid {
|
||||
grid-template-columns: repeat(4, 25%);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.product-form-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 10px 20px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.18);
|
||||
transition: 150ms ease-in-out;
|
||||
z-index: 10;
|
||||
|
||||
.btn {
|
||||
margin-right: 10px;
|
||||
padding: 6px 14px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
border: 1px solid #d9d9d9;
|
||||
background: #ffffff;
|
||||
|
||||
&:hover {
|
||||
color: #0068e1;
|
||||
border-color: #0074fb;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
color: #0068e1;
|
||||
background: #ffffff;
|
||||
border: 1px solid #0068e1;
|
||||
|
||||
&.btn-loading {
|
||||
&:after {
|
||||
border-color: #0068e1;
|
||||
border-right-color: transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:after {
|
||||
border-color: #ffffff;
|
||||
border-right-color: #0068e1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #ffffff;
|
||||
background: #0068e1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-form-left-column {
|
||||
padding-right: 10px;
|
||||
|
||||
.box {
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-form-right-column {
|
||||
padding-left: 10px;
|
||||
|
||||
.box {
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-form-column {
|
||||
&.dragging {
|
||||
border-radius: 2px;
|
||||
outline: 2px dashed #a3b0c2;
|
||||
}
|
||||
|
||||
.alert {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-wrapper {
|
||||
@@ -20,3 +394,350 @@
|
||||
padding: 10px 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.product-downloads-wrapper {
|
||||
.slide {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.table {
|
||||
> tbody {
|
||||
> tr {
|
||||
> td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.options {
|
||||
.drag-handle {
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.choose-file-group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.downloadable-file-name {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.btn-choose-file {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.ltr {
|
||||
&.sidebar-collapse {
|
||||
.product-form {
|
||||
.product-form-footer {
|
||||
left: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-form {
|
||||
.product-form-footer {
|
||||
left: 250px;
|
||||
right: 0;
|
||||
|
||||
.btn-default {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rtl {
|
||||
&.sidebar-collapse {
|
||||
.product-form {
|
||||
.product-form-footer {
|
||||
right: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-form {
|
||||
.product-form-footer {
|
||||
right: 250px;
|
||||
left: 0;
|
||||
|
||||
.btn-default {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1200px) and (max-width: 1649px) {
|
||||
.product-form-right-column {
|
||||
.box-body {
|
||||
> .form-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
> .col-sm-3,
|
||||
> .col-sm-9 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: 5px;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1650px) {
|
||||
.product-form {
|
||||
.variants-group {
|
||||
.product-media-grid {
|
||||
grid-template-columns: repeat(3, 33.33333333333333%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-form-right-column {
|
||||
.product-media-grid {
|
||||
grid-template-columns: repeat(4, 25%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1500px) {
|
||||
.product-form {
|
||||
.bulk-edit-variants {
|
||||
.product-media-grid {
|
||||
grid-template-columns: repeat(4, 25%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1380px) {
|
||||
.product-form-right-column {
|
||||
.product-media-grid {
|
||||
grid-template-columns: repeat(3, 33.33333333333333%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1199px) {
|
||||
.product-form-left-column {
|
||||
padding-right: 15px;
|
||||
|
||||
.box {
|
||||
&:last-child {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-form-right-column {
|
||||
padding-left: 15px;
|
||||
|
||||
.product-media-grid {
|
||||
grid-template-columns: repeat(8, 12.5%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 991px) {
|
||||
.form-horizontal {
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.control-label {
|
||||
margin-bottom: 0;
|
||||
padding-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox,
|
||||
.switch {
|
||||
padding-top: 8px;
|
||||
|
||||
label {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-form {
|
||||
.product-form-footer {
|
||||
position: relative;
|
||||
bottom: auto;
|
||||
margin-top: 20px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ltr,
|
||||
.rtl {
|
||||
.product-form {
|
||||
.product-form-footer {
|
||||
left: auto;
|
||||
right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.form-horizontal {
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
.control-label {
|
||||
margin-bottom: 6px;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox,
|
||||
.switch {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.product-form {
|
||||
.bulk-edit-variants {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bulk-edit-variants,
|
||||
.variants-group {
|
||||
.product-media-grid {
|
||||
grid-template-columns: repeat(7, 14.28571428571429%);
|
||||
}
|
||||
}
|
||||
|
||||
.variants-group {
|
||||
.panel-body {
|
||||
> .row {
|
||||
> .col-sm-4 {
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
> .col-sm-8 {
|
||||
padding-left: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.variant-fields {
|
||||
margin-top: 15px;
|
||||
|
||||
> .row {
|
||||
> .col-sm-6 {
|
||||
&:nth-child(1) {
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
padding-left: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-form-right-column {
|
||||
.product-media-grid {
|
||||
grid-template-columns: repeat(6, 16.66666666666667%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 650px) {
|
||||
.product-form {
|
||||
.bulk-edit-variants,
|
||||
.variants-group {
|
||||
.product-media-grid {
|
||||
grid-template-columns: repeat(6, 16.66666666666667%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 560px) {
|
||||
.product-form .bulk-edit-variants,
|
||||
.product-form .variants-group,
|
||||
.product-form-right-column {
|
||||
.product-media-grid {
|
||||
grid-template-columns: repeat(5, 20%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 420px) {
|
||||
.product-form .bulk-edit-variants,
|
||||
.product-form .variants-group,
|
||||
.product-form-right-column {
|
||||
.product-media-grid {
|
||||
grid-template-columns: repeat(4, 25%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.box-header {
|
||||
padding: 8px 15px;
|
||||
}
|
||||
|
||||
.box-body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.product-downloads-wrapper {
|
||||
.table {
|
||||
> tbody {
|
||||
> tr {
|
||||
> td {
|
||||
&:nth-child(2) {
|
||||
min-width: 280px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 435px) {
|
||||
.product-form {
|
||||
.accordion-box-content {
|
||||
.accordion-box-footer {
|
||||
flex-direction: column;
|
||||
|
||||
> .btn {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 370px) {
|
||||
.product-form {
|
||||
.product-form-footer {
|
||||
flex-direction: column;
|
||||
|
||||
.btn {
|
||||
margin-right: 0;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,50 @@ return [
|
||||
'manage_stock' => 'Inventory Management',
|
||||
'qty' => 'Qty',
|
||||
'in_stock' => 'Stock Availability',
|
||||
'new_from' => 'Product New From',
|
||||
'new_to' => 'Product New To',
|
||||
'new_from' => 'New From',
|
||||
'new_to' => 'New To',
|
||||
'up_sells' => 'Up-Sells',
|
||||
'cross_sells' => 'Cross-Sells',
|
||||
'related_products' => 'Related Products',
|
||||
|
||||
# product attributes
|
||||
'attributes' => [
|
||||
'*.attribute_id' => 'Attribute',
|
||||
'*.values' => 'Values',
|
||||
],
|
||||
|
||||
# product options
|
||||
'options' => [
|
||||
'*.name' => 'Name',
|
||||
'*.type' => 'Type',
|
||||
'*.values.*.label' => 'Label',
|
||||
'*.values.*.price' => 'Price',
|
||||
'*.values.*.price_type' => 'Price Type',
|
||||
],
|
||||
|
||||
# product variations
|
||||
'variations' => [
|
||||
'*.name' => 'Name',
|
||||
'*.type' => 'Type',
|
||||
'*.values' => 'Values',
|
||||
'*.values.*.label' => 'Label',
|
||||
'*.values.*.color' => 'Color',
|
||||
'*.values.*.image' => 'Image',
|
||||
],
|
||||
|
||||
# product variants
|
||||
'variants' => [
|
||||
'*.name' => 'Name',
|
||||
'*.sku' => 'SKU',
|
||||
'*.is_active' => 'Status',
|
||||
'*.is_default' => 'Default',
|
||||
'*.price' => 'Price',
|
||||
'*.special_price' => 'Special Price',
|
||||
'*.special_price_type' => 'Special Price Type',
|
||||
'*.special_price_start' => 'Special Price Start',
|
||||
'*.special_price_end' => 'Special Price End',
|
||||
'*.manage_stock' => 'Inventory Management',
|
||||
'*.qty' => 'Quantity',
|
||||
'*.in_stock' => 'Stock Availability',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -3,31 +3,85 @@
|
||||
return [
|
||||
'product' => 'Product',
|
||||
'products' => 'Products',
|
||||
'save' => 'Save',
|
||||
'save_and_exit' => 'Save & Exit',
|
||||
'save_and_edit' => 'Save & Edit',
|
||||
|
||||
'section' => [
|
||||
'expand_all' => 'Expand All',
|
||||
'collapse_all' => 'Collapse All',
|
||||
'order_saved' => 'Order saved',
|
||||
],
|
||||
|
||||
'table' => [
|
||||
'thumbnail' => 'Thumbnail',
|
||||
'name' => 'Name',
|
||||
'price' => 'Price',
|
||||
],
|
||||
'tabs' => [
|
||||
'group' => [
|
||||
'basic_information' => 'Basic Information',
|
||||
'advanced_information' => 'Advanced Information',
|
||||
],
|
||||
|
||||
'group' => [
|
||||
'general' => 'General',
|
||||
'price' => 'Price',
|
||||
'inventory' => 'Inventory',
|
||||
'images' => 'Images',
|
||||
'attributes' => 'Attributes',
|
||||
'downloads' => 'Downloads',
|
||||
'variations' => 'Variations',
|
||||
'variants' => 'Variants',
|
||||
'options' => 'Options',
|
||||
'pricing' => 'Pricing',
|
||||
'inventory' => 'Inventory',
|
||||
'media' => 'Media',
|
||||
'linked_products' => 'Linked Products',
|
||||
'seo' => 'SEO',
|
||||
'related_products' => 'Related Products',
|
||||
'up_sells' => 'Up-Sells',
|
||||
'cross_sells' => 'Cross-Sells',
|
||||
'additional' => 'Additional',
|
||||
],
|
||||
|
||||
'attributes' => [
|
||||
'attribute' => 'Attribute',
|
||||
'values' => 'Values',
|
||||
'add_attribute' => 'Add Attribute',
|
||||
],
|
||||
|
||||
'downloads' => [
|
||||
'file' => 'File',
|
||||
'choose' => 'Choose',
|
||||
'add_file' => 'Add File',
|
||||
],
|
||||
|
||||
'variations' => [
|
||||
'new_variation' => 'New Variation',
|
||||
'add_variation' => 'Add Variation',
|
||||
'insert' => 'Insert',
|
||||
'add_row' => 'Add Row',
|
||||
'please_add_some_variations' => 'Please add some variations to generate variants',
|
||||
],
|
||||
|
||||
'variants' => [
|
||||
'variants' => 'Variants',
|
||||
'variant' => 'Variant',
|
||||
'default' => 'Default',
|
||||
'inactive' => 'Inactive',
|
||||
'out_of_stock' => 'Out of Stock',
|
||||
'apply' => 'Apply',
|
||||
'has_product_variant' => 'Managed from individual variants',
|
||||
'bulk_variants_updated' => 'Variants updated',
|
||||
'variants_created' => ':count :suffix created',
|
||||
'variants_updated' => ':count :suffix updated',
|
||||
'variants_removed' => ':count :suffix removed',
|
||||
'variants_reordered' => 'Variants reordered',
|
||||
'disable_default_variant' => 'The default variant cannot be disabled',
|
||||
],
|
||||
|
||||
'options' => [
|
||||
'new_option' => 'New Option',
|
||||
'add_option' => 'Add Option',
|
||||
'add_row' => 'Add Row',
|
||||
'insert' => 'Insert',
|
||||
'option_inserted' => 'Option inserted',
|
||||
],
|
||||
|
||||
'form' => [
|
||||
'the_product_won\'t_be_shipped' => 'The product won\'t be shipped',
|
||||
'enable_the_product' => 'Enable the product',
|
||||
'price_types' => [
|
||||
'special_price_types' => [
|
||||
'fixed' => 'Fixed',
|
||||
'percent' => 'Percent',
|
||||
],
|
||||
@@ -39,12 +93,93 @@ return [
|
||||
'1' => 'In Stock',
|
||||
'0' => 'Out of Stock',
|
||||
],
|
||||
'base_image' => 'Base Image',
|
||||
'additional_images' => 'Additional Images',
|
||||
'downloadable_files' => 'Downloadable Files',
|
||||
'file' => 'File',
|
||||
'choose' => 'Choose',
|
||||
'delete_file' => 'Delete File',
|
||||
'add_new_file' => 'Add New File',
|
||||
|
||||
'variations' => [
|
||||
'name' => 'Name',
|
||||
'type' => 'Type',
|
||||
'values' => 'Values',
|
||||
'label' => 'Label',
|
||||
'color' => 'Color',
|
||||
'image' => 'Image',
|
||||
'variation_types' => [
|
||||
'please_select' => 'Please Select',
|
||||
'text' => 'Text',
|
||||
'color' => 'Color',
|
||||
'image' => 'Image',
|
||||
],
|
||||
'select_template' => 'Select Template',
|
||||
],
|
||||
|
||||
'variants' => [
|
||||
'default_variant' => 'Default Variant',
|
||||
'bulk_edit' => 'Bulk Edit',
|
||||
'all_variants' => 'All Variants',
|
||||
'field_type' => 'Field Type',
|
||||
'name' => 'Name',
|
||||
'media' => 'Media',
|
||||
'sku' => 'SKU',
|
||||
'is_active' => 'Status',
|
||||
'enable_the_variants' => 'Enable the variants',
|
||||
'price' => 'Price',
|
||||
'special_price' => 'Special Price',
|
||||
'special_price_type' => 'Special Price Type',
|
||||
|
||||
'special_price_types' => [
|
||||
'fixed' => 'Fixed',
|
||||
'percent' => 'Percent',
|
||||
],
|
||||
|
||||
'special_price_start' => 'Special Price Start',
|
||||
'special_price_end' => 'Special Price End',
|
||||
'manage_stock' => 'Inventory Management',
|
||||
|
||||
'manage_stock_states' => [
|
||||
0 => 'Don\'t Track Inventory',
|
||||
1 => 'Track Inventory',
|
||||
],
|
||||
|
||||
'qty' => 'Qty',
|
||||
'in_stock' => 'Stock Availability',
|
||||
|
||||
'stock_availability_states' => [
|
||||
0 => 'Out of Stock',
|
||||
1 => 'In Stock',
|
||||
],
|
||||
],
|
||||
|
||||
'options' => [
|
||||
'name' => 'Name',
|
||||
'type' => 'Type',
|
||||
'is_required' => 'Required',
|
||||
'label' => 'Label',
|
||||
'price' => 'Price',
|
||||
'price_type' => 'Price Type',
|
||||
|
||||
'option_types' => [
|
||||
'please_select' => 'Please Select',
|
||||
'text' => 'Text',
|
||||
'field' => 'Field',
|
||||
'textarea' => 'Textarea',
|
||||
'select' => 'Select',
|
||||
'dropdown' => 'Dropdown',
|
||||
'checkbox' => 'Checkbox',
|
||||
'checkbox_custom' => 'Custom Checkbox',
|
||||
'radio' => 'Radio Button',
|
||||
'radio_custom' => 'Custom Radio Button',
|
||||
'multiple_select' => 'Multiple Select',
|
||||
'date' => 'Date',
|
||||
'date_time' => 'Date & Time',
|
||||
'time' => 'Time',
|
||||
],
|
||||
|
||||
'price' => 'Price',
|
||||
|
||||
'price_types' => [
|
||||
'fixed' => 'Fixed',
|
||||
'percent' => 'Percent',
|
||||
],
|
||||
|
||||
'select_template' => 'Select Template',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
|
||||
return [
|
||||
'products' => 'Products',
|
||||
'catalog' => 'Catalog',
|
||||
'create_product' => 'Create Product',
|
||||
'all_products' => 'All Products',
|
||||
];
|
||||
|
||||
5
Modules/Product/Resources/lang/en/validation.php
Normal file
5
Modules/Product/Resources/lang/en/validation.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'price_field_is_required' => 'The price field is required',
|
||||
];
|
||||
@@ -8,11 +8,81 @@
|
||||
@endcomponent
|
||||
|
||||
@section('content')
|
||||
<form method="POST" action="{{ route('admin.products.store') }}" class="form-horizontal" id="product-create-form" enctype="multipart/form-data" novalidate>
|
||||
{{ csrf_field() }}
|
||||
<div id="app" v-cloak>
|
||||
<form
|
||||
class="product-form form-horizontal"
|
||||
@input="errors.clear($event.target.name)"
|
||||
@submit.prevent
|
||||
ref="form"
|
||||
>
|
||||
<div class="row">
|
||||
<div class="product-form-left-column col-lg-8 col-md-12">
|
||||
@include('product::admin.products.layouts.left_column')
|
||||
</div>
|
||||
|
||||
{!! $tabs->render(compact('product')) !!}
|
||||
</form>
|
||||
<div class="product-form-right-column col-lg-4 col-md-12">
|
||||
@include('product::admin.products.layouts.right_column')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="product-form-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
:class="{ 'btn-loading': formSubmissionType === 'save' }"
|
||||
:disabled="formSubmissionType === 'save'"
|
||||
@click="submit({ submissionType: 'save' })"
|
||||
>
|
||||
{{ trans('product::products.save') }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
:class="{ 'btn-loading': formSubmissionType === 'save_and_edit' }"
|
||||
:disabled="formSubmissionType === 'save_and_edit'"
|
||||
@click="submit({ submissionType: 'save_and_edit' })"
|
||||
>
|
||||
{{ trans('product::products.save_and_edit') }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
:class="{ 'btn-loading': formSubmissionType === 'save_and_exit' }"
|
||||
:disabled="formSubmissionType === 'save_and_exit'"
|
||||
@click="submit({ submissionType: 'save_and_exit' })"
|
||||
>
|
||||
{{ trans('product::products.save_and_exit') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@include('product::admin.products.partials.shortcuts')
|
||||
|
||||
@push('globals')
|
||||
<script>
|
||||
FleetCart.data['attribute-sets'] = @json($attributeSets);
|
||||
FleetCart.langs['product::products.section.order_saved'] = '{{ trans('product::products.section.order_saved') }}';
|
||||
FleetCart.langs['product::products.variants.variants'] = '{{ trans('product::products.variants.variants') }}';
|
||||
FleetCart.langs['product::products.variants.variant'] = '{{ trans('product::products.variants.variant') }}';
|
||||
FleetCart.langs['product::products.variants.bulk_variants_updated'] = '{{ trans('product::products.variants.bulk_variants_updated') }}';
|
||||
FleetCart.langs['product::products.variants.variants_created'] = '{{ trans('product::products.variants.variants_created') }}';
|
||||
FleetCart.langs['product::products.variants.variants_removed'] = '{{ trans('product::products.variants.variants_removed') }}';
|
||||
FleetCart.langs['product::products.variants.variants_reordered'] = '{{ trans('product::products.variants.variants_reordered') }}';
|
||||
FleetCart.langs['product::products.variants.disable_default_variant'] = '{{ trans('product::products.variants.disable_default_variant') }}';
|
||||
FleetCart.langs['product::products.options.option_inserted'] = '{{ trans('product::products.options.option_inserted') }}';
|
||||
</script>
|
||||
|
||||
@vite([
|
||||
'Modules/Product/Resources/assets/admin/sass/main.scss',
|
||||
'Modules/Product/Resources/assets/admin/js/create.js',
|
||||
'Modules/Attribute/Resources/assets/admin/sass/main.scss',
|
||||
'Modules/Variation/Resources/assets/admin/sass/main.scss',
|
||||
'Modules/Option/Resources/assets/admin/sass/main.scss',
|
||||
'Modules/Media/Resources/assets/admin/sass/main.scss',
|
||||
'Modules/Media/Resources/assets/admin/js/main.js',
|
||||
])
|
||||
@endpush
|
||||
|
||||
@@ -9,12 +9,82 @@
|
||||
@endcomponent
|
||||
|
||||
@section('content')
|
||||
<form method="POST" action="{{ route('admin.products.update', $product) }}" class="form-horizontal" id="product-edit-form" enctype="multipart/form-data" novalidate>
|
||||
{{ csrf_field() }}
|
||||
{{ method_field('put') }}
|
||||
<div id="app" v-cloak>
|
||||
<form
|
||||
class="product-form form-horizontal"
|
||||
@input="errors.clear($event.target.name)"
|
||||
@submit.prevent
|
||||
ref="form"
|
||||
>
|
||||
<div class="row">
|
||||
<div class="product-form-left-column col-lg-8 col-md-12">
|
||||
@include('product::admin.products.layouts.left_column')
|
||||
</div>
|
||||
|
||||
{!! $tabs->render(compact('product')) !!}
|
||||
</form>
|
||||
<div class="product-form-right-column col-lg-4 col-md-12">
|
||||
@include('product::admin.products.layouts.right_column')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="product-form-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
:class="{ 'btn-loading': formSubmissionType === 'save' }"
|
||||
:disabled="formSubmissionType === 'save'"
|
||||
@click="submit({ submissionType: 'save' })"
|
||||
>
|
||||
{{ trans('product::products.save') }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
:class="{ 'btn-loading': formSubmissionType === 'save_and_exit' }"
|
||||
:disabled="formSubmissionType === 'save_and_exit'"
|
||||
@click="submit({ submissionType: 'save_and_exit' })"
|
||||
>
|
||||
{{ trans('product::products.save_and_exit') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@include('product::admin.products.partials.shortcuts')
|
||||
|
||||
@if (session()->has('exit_flash'))
|
||||
@push('notifications')
|
||||
<div class="alert alert-success fade in alert-dismissible clearfix">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
|
||||
<span class="alert-text">{{ session('exit_flash') }}</span>
|
||||
</div>
|
||||
@endpush
|
||||
@endif
|
||||
|
||||
@push('globals')
|
||||
<script>
|
||||
FleetCart.data['product'] = {!! $product_resource !!};
|
||||
FleetCart.data['attribute-sets'] = @json($attributeSets);
|
||||
FleetCart.langs['product::products.section.order_saved'] = '{{ trans('product::products.section.order_saved') }}';
|
||||
FleetCart.langs['product::products.variants.variants'] = '{{ trans('product::products.variants.variants') }}';
|
||||
FleetCart.langs['product::products.variants.variant'] = '{{ trans('product::products.variants.variant') }}';
|
||||
FleetCart.langs['product::products.variants.bulk_variants_updated'] = '{{ trans('product::products.variants.bulk_variants_updated') }}';
|
||||
FleetCart.langs['product::products.variants.variants_created'] = '{{ trans('product::products.variants.variants_created') }}';
|
||||
FleetCart.langs['product::products.variants.variants_removed'] = '{{ trans('product::products.variants.variants_removed') }}';
|
||||
FleetCart.langs['product::products.variants.variants_reordered'] = '{{ trans('product::products.variants.variants_reordered') }}';
|
||||
FleetCart.langs['product::products.variants.disable_default_variant'] = '{{ trans('product::products.variants.disable_default_variant') }}';
|
||||
FleetCart.langs['product::products.options.option_inserted'] = '{{ trans('product::products.options.option_inserted') }}';
|
||||
</script>
|
||||
|
||||
@vite([
|
||||
'Modules/Product/Resources/assets/admin/sass/main.scss',
|
||||
'Modules/Product/Resources/assets/admin/js/edit.js',
|
||||
'Modules/Attribute/Resources/assets/admin/sass/main.scss',
|
||||
'Modules/Variation/Resources/assets/admin/sass/main.scss',
|
||||
'Modules/Option/Resources/assets/admin/sass/main.scss',
|
||||
'Modules/Media/Resources/assets/admin/sass/main.scss',
|
||||
'Modules/Media/Resources/assets/admin/js/main.js',
|
||||
])
|
||||
@endpush
|
||||
|
||||
@@ -16,8 +16,18 @@
|
||||
@endslot
|
||||
@endcomponent
|
||||
|
||||
@if (session()->has('exit_flash'))
|
||||
@push('notifications')
|
||||
<div class="alert alert-success fade in alert-dismissible clearfix">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
|
||||
<span class="alert-text">{{ session('exit_flash') }}</span>
|
||||
</div>
|
||||
@endpush
|
||||
@endif
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
<script type="module">
|
||||
new DataTable('#products-table .table', {
|
||||
columns: [
|
||||
{ data: 'checkbox', orderable: false, searchable: false, width: '3%' },
|
||||
@@ -26,7 +36,7 @@
|
||||
{ data: 'name', name: 'translations.name', orderable: false, defaultContent: '' },
|
||||
{ data: 'price', searchable: false },
|
||||
{ data: 'status', name: 'is_active', searchable: false },
|
||||
{ data: 'created', name: 'created_at' },
|
||||
{ data: 'updated', name: 'updated_at' },
|
||||
],
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
@include('product::admin.products.layouts.sections.general')
|
||||
|
||||
<draggable
|
||||
animation="150"
|
||||
class="product-form-column"
|
||||
:class="{ dragging: isLeftColumnSectionDragging }"
|
||||
data-name="product-form-left-sections"
|
||||
force-fallback="true"
|
||||
handle=".drag-handle"
|
||||
:list="formLeftSections"
|
||||
:store="storeFormSections"
|
||||
@choose="isLeftColumnSectionDragging = true"
|
||||
@unchoose="isLeftColumnSectionDragging = false"
|
||||
@change="notifySectionOrderChange"
|
||||
>
|
||||
<div class="box" v-for="(section, sectionIndex) in formLeftSections" :data-id="section" :key="sectionIndex">
|
||||
@include('product::admin.products.layouts.sections.attributes')
|
||||
@include('product::admin.products.layouts.sections.downloads')
|
||||
@include('product::admin.products.layouts.sections.variations')
|
||||
@include('product::admin.products.layouts.sections.variants')
|
||||
@include('product::admin.products.layouts.sections.options')
|
||||
</div>
|
||||
</draggable>
|
||||
@@ -0,0 +1,22 @@
|
||||
<draggable
|
||||
animation="150"
|
||||
class="product-form-column"
|
||||
:class="{ dragging: isRightColumnSectionDragging }"
|
||||
data-name="product-form-right-sections"
|
||||
force-fallback="true"
|
||||
handle=".drag-handle"
|
||||
:list="formRightSections"
|
||||
:store="storeFormSections"
|
||||
@choose="isRightColumnSectionDragging = true"
|
||||
@unchoose="isRightColumnSectionDragging = false"
|
||||
@change="notifySectionOrderChange"
|
||||
>
|
||||
<div class="box" v-for="(section, index) in formRightSections" :data-id="section" :key="index">
|
||||
@include('product::admin.products.layouts.sections.pricing')
|
||||
@include('product::admin.products.layouts.sections.inventory')
|
||||
@include('product::admin.products.layouts.sections.media')
|
||||
@include('product::admin.products.layouts.sections.linked_products')
|
||||
@include('product::admin.products.layouts.sections.seo')
|
||||
@include('product::admin.products.layouts.sections.additional')
|
||||
</div>
|
||||
</draggable>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template v-else-if="section === 'additional'">
|
||||
<div class="box-header">
|
||||
<h5>{{ trans('product::products.group.additional') }}</h5>
|
||||
|
||||
<div class="drag-handle">
|
||||
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
|
||||
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box-body">
|
||||
<div class="form-group">
|
||||
<label for="short-description" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.short_description') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-9">
|
||||
<textarea name="short_description" rows="6" cols="10" id="short-description" class="form-control" v-model="form.short_description"></textarea>
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('short_description')" v-text="errors.get('short_description')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="new-from" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.new_from') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-calendar" aria-hidden="true"></i>
|
||||
</span>
|
||||
|
||||
<flat-pickr
|
||||
name="new_from"
|
||||
id="new-from"
|
||||
class="form-control"
|
||||
:config="flatPickrConfig"
|
||||
v-model="form.new_from"
|
||||
>
|
||||
</flat-pickr>
|
||||
</div>
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('new_from')" v-text="errors.get('new_from')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="new-to" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.new_to') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-calendar" aria-hidden="true"></i>
|
||||
</span>
|
||||
|
||||
<flat-pickr
|
||||
name="new_to"
|
||||
id="new-to"
|
||||
class="form-control"
|
||||
:config="flatPickrConfig"
|
||||
v-model="form.new_to"
|
||||
>
|
||||
</flat-pickr>
|
||||
</div>
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('new_to')" v-text="errors.get('new_to')"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,122 @@
|
||||
<template v-if="section === 'attributes'">
|
||||
<div class="box-header">
|
||||
<h5>{{ trans('product::products.group.attributes') }}</h5>
|
||||
|
||||
<div class="drag-handle">
|
||||
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
|
||||
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box-body">
|
||||
<div id="product-attributes-wrapper">
|
||||
<div class="table-responsive">
|
||||
<table class="options table table-bordered">
|
||||
<thead class="hidden-xs">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{ trans('product::products.attributes.attribute') }}</th>
|
||||
<th>{{ trans('product::products.attributes.values') }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody
|
||||
animation="150"
|
||||
handle=".drag-handle"
|
||||
is="draggable"
|
||||
tag="tbody"
|
||||
:list="form.attributes"
|
||||
>
|
||||
<tr v-for="(attribute, index) in form.attributes" :key="index">
|
||||
<td class="text-center">
|
||||
<span class="drag-handle">
|
||||
<i class="fa"></i>
|
||||
<i class="fa"></i>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div class="form-group">
|
||||
<label :for="`attributes-${attribute.uid}-attribute-id`" class="visible-xs">
|
||||
{{ trans('product::products.attributes.attribute') }}
|
||||
</label>
|
||||
|
||||
<select
|
||||
:name="`attributes.${attribute.uid}.attribute_id`"
|
||||
:id="`attributes-${attribute.uid}-attribute-id`"
|
||||
class="form-control attribute custom-select-black"
|
||||
@change="focusAttributeValueField(index)"
|
||||
v-model.number="attribute.attribute_id"
|
||||
>
|
||||
<option value="">{{ trans('admin::admin.form.please_select') }}</option>
|
||||
|
||||
@foreach ($attributeSets as $attributeSet)
|
||||
<optgroup label="{{ $attributeSet->name }}">
|
||||
@foreach ($attributeSet->attributes as $attribute)
|
||||
<option value="{{ $attribute->id }}">
|
||||
{{ $attribute->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</optgroup>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`attributes.${attribute.uid}.attribute_id`)"
|
||||
v-text="errors.get(`attributes.${attribute.uid}.attribute_id`)"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div class="form-group">
|
||||
<label :for="`attributes-${attribute.uid}-values`" class="visible-xs">
|
||||
{{ trans('product::products.attributes.values') }}
|
||||
</label>
|
||||
|
||||
<selectize
|
||||
:name="`attributes.${attribute.uid}.values`"
|
||||
:id="`attributes-${attribute.uid}-values`"
|
||||
:settings="selectizeConfig"
|
||||
@input="clearValuesError({ name: 'attributes', uid: attribute.uid })"
|
||||
v-model="attribute.values"
|
||||
multiple
|
||||
ref="attributeValues"
|
||||
>
|
||||
<option
|
||||
v-for="(value, valueIndex) in getAttributeValuesById(attribute.attribute_id)"
|
||||
:key="valueIndex"
|
||||
:value="value.id"
|
||||
>
|
||||
@{{ value.value }}
|
||||
</option>
|
||||
</selectize>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`attributes.${attribute.uid}.values`)"
|
||||
v-text="errors.get(`attributes.${attribute.uid}.values`)"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-default delete-row" @click="deleteAttribute(index, attribute.uid)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-default" @click="addAttribute">
|
||||
{{ trans('product::products.attributes.add_attribute') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template v-else-if="section === 'downloads'">
|
||||
<div class="box-header">
|
||||
<h5>{{ trans('product::products.group.downloads') }}</h5>
|
||||
|
||||
<div class="drag-handle">
|
||||
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
|
||||
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box-body">
|
||||
<div class="product-downloads-wrapper clearfix">
|
||||
<div class="table-responsive">
|
||||
<table class="options table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{ trans('product::products.downloads.file') }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody
|
||||
animation="150"
|
||||
handle=".drag-handle"
|
||||
is="draggable"
|
||||
tag="tbody"
|
||||
:list="form.downloads"
|
||||
>
|
||||
<tr v-for="(download, index) in form.downloads" :key="index">
|
||||
<td class="text-center">
|
||||
<span class="drag-handle">
|
||||
<i class="fa"></i>
|
||||
<i class="fa"></i>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div class="choose-file-group">
|
||||
<input
|
||||
type="text"
|
||||
:value="download.filename"
|
||||
class="form-control downloadable-file-name"
|
||||
readonly
|
||||
>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default btn-choose-file"
|
||||
@click="chooseDownloadableFile(index)"
|
||||
>
|
||||
{{ trans('product::products.downloads.choose') }}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="text-center">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default delete-row"
|
||||
@click="deleteDownload(index)"
|
||||
>
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-default" @click="addDownload">
|
||||
{{ trans('product::products.downloads.add_file') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,171 @@
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
<h5>{{ trans('product::products.group.general') }}</h5>
|
||||
</div>
|
||||
|
||||
<div class="box-body">
|
||||
<div class="form-group">
|
||||
<label for="name" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.name') }}
|
||||
<span class="text-red">*</span>
|
||||
</label>
|
||||
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
class="form-control"
|
||||
v-model="form.name"
|
||||
|
||||
@if (request()->routeIs('admin.products.create'))
|
||||
@change="setProductSlug($event.target.value)"
|
||||
@endif
|
||||
>
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('name')" v-text="errors.get('name')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description" class="col-sm-3 control-label text-left" @click="focusEditor">
|
||||
{{ trans('product::attributes.description') }}
|
||||
<span class="text-red">*</span>
|
||||
</label>
|
||||
|
||||
<div class="col-sm-9">
|
||||
<textarea
|
||||
name="description"
|
||||
id="description"
|
||||
class="form-control wysiwyg"
|
||||
v-model="form.description"
|
||||
>
|
||||
</textarea>
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('description')" v-text="errors.get('description')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="brand-id" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.brand_id') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<select name="brand_id" id="brand-id" class="form-control custom-select-black" v-model="form.brand_id">
|
||||
@foreach ($brands as $id => $label)
|
||||
<option value="{{ $id }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('brand_id')" v-text="errors.get('brand_id')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="categories" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.categories') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<selectize
|
||||
name="categories"
|
||||
id="categories"
|
||||
:settings="categoriesSelectizeConfig"
|
||||
v-model="form.categories"
|
||||
multiple
|
||||
>
|
||||
@foreach ($categories as $id => $label)
|
||||
<option value="{{ $id }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</selectize>
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('categories')" v-text="errors.get('categories')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="tax-class-id" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.tax_class_id') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<select name="tax_class_id" id="tax-class-id" class="form-control custom-select-black" v-model="form.tax_class_id">
|
||||
@foreach ($taxClasses as $id => $label)
|
||||
<option value="{{ $id }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('tax_class_id')" v-text="errors.get('tax_class_id')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="tags" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.tags') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<selectize
|
||||
name="tags"
|
||||
id="tags"
|
||||
:settings="selectizeConfig"
|
||||
v-model="form.tags"
|
||||
multiple
|
||||
>
|
||||
@foreach ($tags as $id => $label)
|
||||
<option value="{{ $id }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</selectize>
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('tags')" v-text="errors.get('tags')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="is_virtual" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.is_virtual') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<div class="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="is_virtual"
|
||||
id="is-virtual"
|
||||
v-model="form.is_virtual"
|
||||
>
|
||||
|
||||
<label for="is-virtual">
|
||||
{{ trans('product::products.form.the_product_won\'t_be_shipped') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('is_virtual')" v-text="errors.get('is_virtual')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="is-active" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.is_active') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-9">
|
||||
<div class="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="is_active"
|
||||
id="is-active"
|
||||
v-model="form.is_active"
|
||||
>
|
||||
|
||||
<label for="is-active">
|
||||
{{ trans('product::products.form.enable_the_product') }}
|
||||
</label>
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('is_active')" v-text="errors.get('is_active')"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,101 @@
|
||||
<template v-else-if="section === 'inventory'">
|
||||
<div class="box-header">
|
||||
<h5>{{ trans('product::products.group.inventory') }}</h5>
|
||||
|
||||
<div class="drag-handle">
|
||||
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
|
||||
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box-body">
|
||||
<div v-if="hasAnyVariant" class="alert alert-info">
|
||||
{{ trans('product::products.variants.has_product_variant') }}
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="form-group">
|
||||
<label for="sku" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.sku') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
type="text"
|
||||
name="sku"
|
||||
id="sku"
|
||||
class="form-control"
|
||||
v-model="form.sku"
|
||||
>
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('sku')" v-text="errors.get('sku')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="manage-stock" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.manage_stock') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-9">
|
||||
<select
|
||||
name="manage_stock"
|
||||
id="manage-stock"
|
||||
class="form-control custom-select-black"
|
||||
@change="focusField({
|
||||
selector: '#qty'
|
||||
})"
|
||||
v-model.number="form.manage_stock"
|
||||
>
|
||||
<option value="0">{{ trans('product::products.form.manage_stock_states.0') }}</option>
|
||||
<option value="1">{{ trans('product::products.form.manage_stock_states.1') }}</option>
|
||||
</select>
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('manage_stock')" v-text="errors.get('manage_stock')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" v-if="form.manage_stock == 1">
|
||||
<label for="qty" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.qty') }}
|
||||
<span class="text-red">*</span>
|
||||
</label>
|
||||
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
name="qty"
|
||||
step="1"
|
||||
id="qty"
|
||||
class="form-control"
|
||||
@wheel="$event.target.blur()"
|
||||
v-model.number="form.qty"
|
||||
>
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('qty')" v-text="errors.get('qty')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="in-stock" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.in_stock') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-9">
|
||||
<select
|
||||
name="in_stock"
|
||||
id="in-stock"
|
||||
class="form-control custom-select-black"
|
||||
v-model.number="form.in_stock"
|
||||
>
|
||||
<option value="1">{{ trans('product::products.form.stock_availability_states.1') }}</option>
|
||||
<option value="0">{{ trans('product::products.form.stock_availability_states.0') }}</option>
|
||||
</select>
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('in_stock')" v-text="errors.get('in_stock')"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,72 @@
|
||||
<template v-else-if="section === 'linked_products'">
|
||||
<div class="box-header">
|
||||
<h5>{{ trans('product::products.group.linked_products') }}</h5>
|
||||
|
||||
<div class="drag-handle">
|
||||
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
|
||||
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box-body">
|
||||
<div class="form-group">
|
||||
<label for="up-sells" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.up_sells') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-9">
|
||||
<selectize
|
||||
name="up_sells"
|
||||
id="up-sells"
|
||||
:settings="searchableSelectizeConfig"
|
||||
v-model="form.up_sells"
|
||||
multiple
|
||||
>
|
||||
@foreach ($product->upSellProducts as $upSellProduct)
|
||||
<option value="{{ $upSellProduct->id }}">{{ $upSellProduct->name }}</option>
|
||||
@endforeach
|
||||
</selectize>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="cross-sells" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.cross_sells') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-9">
|
||||
<selectize
|
||||
name="cross_sells"
|
||||
id="cross-sells"
|
||||
:settings="searchableSelectizeConfig"
|
||||
v-model="form.cross_sells"
|
||||
multiple
|
||||
>
|
||||
@foreach ($product->crossSellProducts as $crossSellProduct)
|
||||
<option value="{{ $crossSellProduct->id }}">{{ $crossSellProduct->name }}</option>
|
||||
@endforeach
|
||||
</selectize>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="related-products" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.related_products') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-9">
|
||||
<selectize
|
||||
name="related_products"
|
||||
id="related-products"
|
||||
:settings="searchableSelectizeConfig"
|
||||
v-model="form.related_products"
|
||||
multiple
|
||||
>
|
||||
@foreach ($product->relatedProducts as $relatedProduct)
|
||||
<option value="{{ $relatedProduct->id }}">{{ $relatedProduct->name }}</option>
|
||||
@endforeach
|
||||
</selectize>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template v-if="section === 'media'">
|
||||
<div class="box-header">
|
||||
<h5>{{ trans('product::products.group.media') }}</h5>
|
||||
|
||||
<div class="drag-handle">
|
||||
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
|
||||
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box-body">
|
||||
<div v-if="hasAnyVariant" class="alert alert-info">
|
||||
{{ trans('product::products.variants.has_product_variant') }}
|
||||
</div>
|
||||
|
||||
<div v-else class="row">
|
||||
<div class="col-md-12">
|
||||
<draggable
|
||||
animation="200"
|
||||
class="product-media-grid"
|
||||
force-fallback="true"
|
||||
handle=".handle"
|
||||
:move="preventLastSlideDrag"
|
||||
:list="form.media"
|
||||
>
|
||||
<div class="media-grid-item handle" v-for="(media, index) in form.media" :key="index">
|
||||
<div class="image-holder">
|
||||
<img :src="media.path" alt="product media">
|
||||
|
||||
<button type="button" class="btn remove-image" @click="removeMedia(index)"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="media-grid-item media-picker disabled" @click="addMedia">
|
||||
<div class="image-holder">
|
||||
<img src="{{ asset('build/assets/placeholder_image.png') }}" class="placeholder-image" alt="Placeholder image">
|
||||
</div>
|
||||
</div>
|
||||
</draggable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,436 @@
|
||||
<template v-else-if="section === 'options'">
|
||||
<div class="box-header">
|
||||
<h5>{{ trans('product::products.group.options') }}</h5>
|
||||
|
||||
<div class="d-flex">
|
||||
<span
|
||||
class="toggle-accordion"
|
||||
:class="{ 'collapsed': isCollapsedOptionsAccordion }"
|
||||
data-toggle="tooltip"
|
||||
data-placement="top"
|
||||
:data-original-title="
|
||||
isCollapsedOptionsAccordion ?
|
||||
'{{ trans('product::products.section.expand_all') }}' :
|
||||
'{{ trans('product::products.section.collapse_all') }}'
|
||||
"
|
||||
@click="toggleAccordions({
|
||||
selector: '.options-group .panel-heading',
|
||||
state: isCollapsedOptionsAccordion,
|
||||
data: form.options
|
||||
})"
|
||||
>
|
||||
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
||||
</span>
|
||||
|
||||
<div class="drag-handle">
|
||||
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
|
||||
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box-body clearfix">
|
||||
<div class="accordion-box-content">
|
||||
<draggable
|
||||
animation="150"
|
||||
class="options-group"
|
||||
force-fallback="true"
|
||||
handle=".drag-handle"
|
||||
:list="form.options"
|
||||
>
|
||||
<div
|
||||
v-for="(option, index) in form.options"
|
||||
:id="`option-${option.uid}`"
|
||||
class="content-accordion panel-group options-group-wrapper"
|
||||
:class="`option-${option.uid}`"
|
||||
:key="index"
|
||||
>
|
||||
<div class="panel panel-default option">
|
||||
<div class="panel-heading" @click.stop="toggleAccordion($event, option)">
|
||||
<h4 class="panel-title">
|
||||
<div
|
||||
:aria-expanded="option.is_open"
|
||||
data-toggle="collapse"
|
||||
data-transition="false"
|
||||
:href="`#custom-collapse-${option.uid}`"
|
||||
:class="{
|
||||
'collapsed': !option.is_open,
|
||||
'has-error': hasAnyError({
|
||||
name: 'options',
|
||||
uid: option.uid
|
||||
})
|
||||
}"
|
||||
>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="drag-handle">
|
||||
<i class="fa"></i>
|
||||
<i class="fa"></i>
|
||||
</span>
|
||||
|
||||
<span v-text="option.name || '{{ trans('product::products.options.new_option') }}'"></span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="delete-option"
|
||||
@click.stop="deleteOption(index, option.uid)"
|
||||
>
|
||||
<i class="fa fa-trash"></i>
|
||||
</span>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="panel-collapse" :class="{ 'collapse': !option.is_open }">
|
||||
<div class="panel-body">
|
||||
<div class="new-option">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label :for="`options-${option.uid}-name`">
|
||||
{{ trans('product::products.form.options.name') }}
|
||||
<span v-if="option.name || option.type" class="text-red">*</span>
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
:name="`options.${option.uid}.name`"
|
||||
class="form-control option-name-field"
|
||||
:id="`options-${option.uid}-name`"
|
||||
v-model="option.name"
|
||||
>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`options.${option.uid}.name`)"
|
||||
v-text="errors.get(`options.${option.uid}.name`)"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3">
|
||||
<div class="form-group">
|
||||
<label :for="`options-${option.uid}-type`">
|
||||
{{ trans('product::products.form.options.type') }}
|
||||
<span v-if="option.name || option.type" class="text-red">*</span>
|
||||
</label>
|
||||
|
||||
<select
|
||||
:name="`options.${option.uid}.type`"
|
||||
:id="`options-${option.uid}-type`"
|
||||
class="form-control custom-select-black"
|
||||
@change="changeOptionType(index, option.uid)"
|
||||
v-model="option.type"
|
||||
>
|
||||
<option value="">
|
||||
{{ trans('product::products.form.options.option_types.please_select') }}
|
||||
</option>
|
||||
|
||||
<optgroup label="{{ trans('product::products.form.options.option_types.text') }}">
|
||||
<option value="field">
|
||||
{{ trans('product::products.form.options.option_types.field') }}
|
||||
</option>
|
||||
|
||||
<option value="textarea">
|
||||
{{ trans('product::products.form.options.option_types.textarea') }}
|
||||
</option>
|
||||
</optgroup>
|
||||
|
||||
<optgroup label="{{ trans('product::products.form.options.option_types.select') }}">
|
||||
<option value="dropdown">
|
||||
{{ trans('product::products.form.options.option_types.dropdown') }}
|
||||
</option>
|
||||
|
||||
<option value="checkbox">
|
||||
{{ trans('product::products.form.options.option_types.checkbox') }}
|
||||
</option>
|
||||
|
||||
<option value="checkbox_custom">
|
||||
{{ trans('product::products.form.options.option_types.checkbox_custom') }}
|
||||
</option>
|
||||
|
||||
<option value="radio">
|
||||
{{ trans('product::products.form.options.option_types.radio') }}
|
||||
</option>
|
||||
|
||||
<option value="radio_custom">
|
||||
{{ trans('product::products.form.options.option_types.radio_custom') }}
|
||||
</option>
|
||||
|
||||
<option value="multiple_select">
|
||||
{{ trans('product::products.form.options.option_types.multiple_select') }}
|
||||
</option>
|
||||
</optgroup>
|
||||
|
||||
<optgroup label="{{ trans('product::products.form.options.option_types.date') }}">
|
||||
<option value="date">
|
||||
{{ trans('product::products.form.options.option_types.date') }}
|
||||
</option>
|
||||
|
||||
<option value="date_time">
|
||||
{{ trans('product::products.form.options.option_types.date_time') }}
|
||||
</option>
|
||||
|
||||
<option value="time">
|
||||
{{ trans('product::products.form.options.option_types.time') }}
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`options.${option.uid}.type`)"
|
||||
v-text="errors.get(`options.${option.uid}.type`)"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3">
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
:name="`options.${option.uid}.is_required`"
|
||||
:id="`options-${option.uid}-is-required`"
|
||||
class="form-control"
|
||||
v-model="option.is_required"
|
||||
>
|
||||
|
||||
<label :for="`options-${option.uid}-is-required`">
|
||||
{{ trans('product::products.form.options.is_required') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="isOptionTypeText(option)">
|
||||
<div class="option-values" :id="`options.${option.uid}.values`">
|
||||
<div class="table-responsive option-text">
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ trans('product::products.form.options.price') }}</th>
|
||||
<th>{{ trans('product::products.form.options.price_type') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="(value, valueIndex) in option.values" :key="valueIndex">
|
||||
<td>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">
|
||||
@{{ value.price_type === 'fixed' ? '$' : '%' }}
|
||||
</span>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.1"
|
||||
:name="`options.${option.uid}.values.${value.uid}.price`"
|
||||
:id="`options-${option.uid}-values-${value.uid}-price`"
|
||||
class="form-control"
|
||||
@wheel="$event.target.blur()"
|
||||
v-model="value.price"
|
||||
>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`options.${option.uid}.values.${value.uid}.price`)"
|
||||
v-text="errors.get(`options.${option.uid}.values.${value.uid}.price`)"
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<select
|
||||
:name="`options.${option.uid}.values.${value.uid}.price_type`"
|
||||
:id="`options-${option.uid}-values-${value.uid}-price-type`"
|
||||
class="form-control custom-select-black"
|
||||
v-model="value.price_type"
|
||||
>
|
||||
<option value="fixed">
|
||||
{{ trans('product::products.form.options.price_types.fixed') }}
|
||||
</option>
|
||||
|
||||
<option value="percent">
|
||||
{{ trans('product::products.form.options.price_types.fixed') }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`options.${option.uid}.values.${value.uid}.price_type`)"
|
||||
v-text="errors.get(`options.${option.uid}.values.${value.uid}.price_type`)"
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="isOptionTypeSelect(option)">
|
||||
<div class="option-values" :id="`options.${option.uid}.values`">
|
||||
<div class="option-select">
|
||||
<div class="table-responsive option-text">
|
||||
<table class="options table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
{{ trans('product::products.form.options.label') }}
|
||||
<span class="text-red">*</span>
|
||||
</th>
|
||||
<th>{{ trans('product::products.form.options.price') }}</th>
|
||||
<th>{{ trans('product::products.form.options.price_type') }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody
|
||||
animation="150"
|
||||
handle=".drag-handle"
|
||||
is="draggable"
|
||||
tag="tbody"
|
||||
:list="option.values"
|
||||
>
|
||||
<tr v-for="(value, valueIndex) in option.values" :key="valueIndex">
|
||||
<td class="text-center">
|
||||
<span class="drag-handle">
|
||||
<i class="fa"></i>
|
||||
<i class="fa"></i>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
:name="`options.${option.uid}.values.${value.uid}.label`"
|
||||
:id="`options-${option.uid}-values-${value.uid}-label`"
|
||||
class="form-control"
|
||||
@keyup.enter="addOptionRowOnPressEnter($event, index, valueIndex)"
|
||||
v-model="value.label"
|
||||
>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`options.${option.uid}.values.${value.uid}.label`)"
|
||||
v-text="errors.get(`options.${option.uid}.values.${value.uid}.label`)"
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">
|
||||
@{{ value.price_type === 'fixed' ? '$' : '%' }}
|
||||
</span>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.1"
|
||||
:name="`options.${option.uid}.values.${value.uid}.price`"
|
||||
:id="`options-${option.uid}-values-${value.uid}-price`"
|
||||
class="form-control"
|
||||
@wheel="$event.target.blur()"
|
||||
v-model="value.price"
|
||||
>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`options.${option.uid}.values.${value.uid}.price`)"
|
||||
v-text="errors.get(`options.${option.uid}.values.${value.uid}.price`)"
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<select
|
||||
:name="`options.${option.uid}.values.${value.uid}.price_type`"
|
||||
:id="`options-${option.uid}-values-${value.uid}-price-type`"
|
||||
class="form-control custom-select-black"
|
||||
v-model="value.price_type"
|
||||
>
|
||||
<option value="fixed">
|
||||
{{ trans('product::products.form.options.price_types.fixed') }}
|
||||
</option>
|
||||
|
||||
<option value="percent">
|
||||
{{ trans('product::products.form.options.price_types.percent') }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`options.${option.uid}.values.${value.uid}.price_type`)"
|
||||
v-text="errors.get(`options.${option.uid}.values.${value.uid}.price_type`)"
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="btn btn-default delete-row"
|
||||
@click="deleteOptionRow(index, option.uid, valueIndex, value.uid)"
|
||||
>
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
@click="addOptionRow(index, option.uid)"
|
||||
>
|
||||
{{ trans('product::products.options.add_row') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</draggable>
|
||||
|
||||
<div class="accordion-box-footer">
|
||||
<button type="button" class="btn btn-default" @click="addOption">
|
||||
{{ trans('product::products.options.add_option') }}
|
||||
</button>
|
||||
|
||||
@hasAccess('admin.options.index')
|
||||
@if ($globalOptions->isNotEmpty())
|
||||
<div class="insert-template">
|
||||
<select class="form-control custom-select-black" v-model="globalOptionId">
|
||||
<option value="">{{ trans('product::products.form.options.select_template') }}</option>
|
||||
|
||||
@foreach ($globalOptions as $globalOption)
|
||||
<option value="{{ $globalOption->id }}">{{ $globalOption->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
:class="{ 'btn-loading': addingGlobalOption }"
|
||||
:disabled="isAddGlobalOptionDisabled"
|
||||
@click="addGlobalOption"
|
||||
>
|
||||
{{ trans('product::products.options.insert') }}
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
@endHasAccess
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,147 @@
|
||||
<template v-if="section === 'price'">
|
||||
<div class="box-header">
|
||||
<h5>{{ trans('product::products.group.pricing') }}</h5>
|
||||
|
||||
<div class="drag-handle">
|
||||
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
|
||||
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box-body">
|
||||
<div v-if="hasAnyVariant" class="alert alert-info">
|
||||
{{ trans('product::products.variants.has_product_variant') }}
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="form-group">
|
||||
<label for="price" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.price') }}
|
||||
<span class="text-red">*</span>
|
||||
</label>
|
||||
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">$</span>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
name="price"
|
||||
step="0.1"
|
||||
id="price"
|
||||
class="form-control"
|
||||
@wheel="$event.target.blur()"
|
||||
v-model="form.price"
|
||||
>
|
||||
</div>
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('price')" v-text="errors.get('price')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="special-price" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.special_price') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">
|
||||
@{{ form.special_price_type === 'fixed' ? '$' : '%' }}
|
||||
</span>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
name="special_price"
|
||||
step="0.1"
|
||||
id="special-price"
|
||||
class="form-control"
|
||||
@wheel="$event.target.blur()"
|
||||
v-model="form.special_price"
|
||||
>
|
||||
</div>
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('special_price')" v-text="errors.get('special_price')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="special-price-type" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.special_price_type') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-9">
|
||||
<select
|
||||
name="special_price_type"
|
||||
id="special-price-type"
|
||||
class="form-control custom-select-black"
|
||||
v-model="form.special_price_type"
|
||||
|
||||
>
|
||||
<option value="fixed">
|
||||
{{ trans('product::products.form.special_price_types.fixed') }}
|
||||
</option>
|
||||
|
||||
<option value="percent">
|
||||
{{ trans('product::products.form.special_price_types.percent') }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('special_price_type')" v-text="errors.get('special_price_type')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="special-price-start" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.special_price_start') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-calendar" aria-hidden="true"></i>
|
||||
</span>
|
||||
|
||||
<flat-pickr
|
||||
name="special_price_start"
|
||||
id="special-price-start"
|
||||
class="form-control"
|
||||
:config="flatPickrConfig"
|
||||
v-model="form.special_price_start"
|
||||
>
|
||||
</flat-pickr>
|
||||
</div>
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('special_price_start')" v-text="errors.get('special_price_start')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="special-price-end" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.special_price_end') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-calendar" aria-hidden="true"></i>
|
||||
</span>
|
||||
|
||||
<flat-pickr
|
||||
name="special_price_end"
|
||||
id="special-price-end"
|
||||
class="form-control"
|
||||
:config="flatPickrConfig"
|
||||
v-model="form.special_price_end"
|
||||
>
|
||||
</flat-pickr>
|
||||
</div>
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('special_price_end')" v-text="errors.get('special_price_end')"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,49 @@
|
||||
<template v-else-if="section === 'seo'">
|
||||
<div class="box-header">
|
||||
<h5>{{ trans('product::products.group.seo') }}</h5>
|
||||
|
||||
<div class="drag-handle">
|
||||
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
|
||||
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box-body">
|
||||
<div class="form-group">
|
||||
<label for="slug" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::attributes.slug') }}
|
||||
<span v-if="route().current('admin.products.edit')" class="text-red">*</span>
|
||||
</label>
|
||||
|
||||
<div class="col-sm-9">
|
||||
<input type="text" name="slug" id="slug" class="form-control" @change="setProductSlug($event.target.value)" v-model="form.slug">
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('slug')" v-text="errors.get('slug')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="meta-title" class="col-sm-3 control-label text-left">
|
||||
{{ trans('meta::attributes.meta_title') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-9">
|
||||
<input type="text" name="meta.meta_title" id="meta-title" class="form-control" v-model="form.meta.meta_title">
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('meta.meta_title')" v-text="errors.get('meta.meta_title')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="meta-description" class="col-sm-3 control-label text-left">
|
||||
{{ trans('meta::attributes.meta_description') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-9">
|
||||
<textarea name="meta.meta_description" rows="6" cols="10" id="meta-description" class="form-control" v-model="form.meta.meta_description"></textarea>
|
||||
|
||||
<span class="help-block text-red" v-if="errors.has('meta.meta_description')" v-text="errors.get('meta.meta_description')"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,439 @@
|
||||
<template v-else-if="section === 'variants'">
|
||||
<div class="box-header">
|
||||
<h5>{{ trans('product::products.group.variants') }}</h5>
|
||||
|
||||
<div class="d-flex">
|
||||
<span
|
||||
v-if="hasAnyVariant"
|
||||
class="toggle-accordion"
|
||||
:class="{ 'collapsed': isCollapsedVariantsAccordion }"
|
||||
data-toggle="tooltip"
|
||||
data-placement="top"
|
||||
:data-original-title="
|
||||
isCollapsedVariantsAccordion ?
|
||||
'{{ trans('product::products.section.expand_all') }}' :
|
||||
'{{ trans('product::products.section.collapse_all') }}'
|
||||
"
|
||||
@click="toggleAccordions({
|
||||
selector: '.variants-group .panel-heading',
|
||||
state: isCollapsedVariantsAccordion,
|
||||
data: form.variants
|
||||
})"
|
||||
>
|
||||
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
||||
</span>
|
||||
|
||||
<div class="drag-handle">
|
||||
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
|
||||
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box-body">
|
||||
<div class="accordion-box-content">
|
||||
<div v-if="!hasAnyVariant" class="alert alert-info">
|
||||
{{ trans('product::products.variations.please_add_some_variations') }}
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="form-group">
|
||||
<label for="default-variant" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::products.form.variants.default_variant') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<select
|
||||
name="default_variant"
|
||||
id="default-variant"
|
||||
class="form-control custom-select-black"
|
||||
@change="changeDefaultVariant($event.target.value)"
|
||||
>
|
||||
<option
|
||||
v-for="(variant, index) in form.variants"
|
||||
:value="variant.uid"
|
||||
:selected="defaultVariantUid === variant.uid"
|
||||
:disabled="!isActiveVariant(index)"
|
||||
>
|
||||
@{{ variant.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@include('product::admin.products.partials.bulk_edit_variants')
|
||||
|
||||
<transition-group tag="div" name="variant" class="variants-group">
|
||||
<div
|
||||
v-for="(variant, index) in form.variants"
|
||||
:id="`variant-${variant.uid}`"
|
||||
class="content-accordion panel-group options-group-wrapper"
|
||||
:key="variant.position"
|
||||
>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" @click.stop="toggleAccordion($event, variant)">
|
||||
<h4 class="panel-title">
|
||||
<div
|
||||
:aria-expanded="variant.is_open"
|
||||
data-toggle="collapse"
|
||||
:href="`#variant-collapse-${variant.uid}`"
|
||||
:class="{
|
||||
'collapsed': !variant.is_open,
|
||||
'has-error': hasAnyError({
|
||||
name: 'variants',
|
||||
uid: variant.uid
|
||||
})
|
||||
}"
|
||||
>
|
||||
<div class="d-flex align-items-center">
|
||||
<div v-if="variant.is_selected" class="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
:name="`variants.${variant.uid}.is_selected`"
|
||||
:id="`variants-${variant.uid}-is-selected`"
|
||||
:checked="variant.is_selected"
|
||||
disabled
|
||||
>
|
||||
|
||||
<label :for="`variants-${variant.uid}-is-selected`"></label>
|
||||
</div>
|
||||
|
||||
<span class="variant-name">@{{ variant.name }}</span>
|
||||
|
||||
<ul class="variant-badge list-inline d-flex">
|
||||
<li v-if="variant.is_default">
|
||||
<span class="label label-primary">
|
||||
{{ trans('product::products.variants.default') }}
|
||||
</span>
|
||||
</li>
|
||||
<li v-else-if="!variant.is_active">
|
||||
<span class="label label-default">
|
||||
{{ trans('product::products.variants.inactive') }}
|
||||
</span>
|
||||
</li>
|
||||
<li v-if="variant.is_active && variant.in_stock == 0">
|
||||
<span class="label label-warning">
|
||||
{{ trans('product::products.variants.out_of_stock') }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="switch" @click.stop>
|
||||
<input
|
||||
type="checkbox"
|
||||
:name="`variants.${variant.uid}.is_active`"
|
||||
:id="`variants-${variant.uid}-is-active`"
|
||||
:disabled="defaultVariantUid === variant.uid"
|
||||
v-model="variant.is_active"
|
||||
>
|
||||
|
||||
<label :for="`variants-${variant.uid}-is-active`" @click="changeVariantStatus(variant.uid)"></label>
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="panel-collapse" :class="{ 'collapse': !variant.is_open }">
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<draggable
|
||||
animation="200"
|
||||
class="product-media-grid"
|
||||
force-fallback="true"
|
||||
handle=".handle"
|
||||
:move="preventLastSlideDrag"
|
||||
:list="variant.media"
|
||||
>
|
||||
<div class="media-grid-item handle" v-for="(media, mediaIndex) in variant.media" :key="mediaIndex">
|
||||
<div class="image-holder">
|
||||
<img :src="media.path" alt="product variant media">
|
||||
|
||||
<button type="button" class="btn remove-image" @click="removeVariantMedia(index, mediaIndex)"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="media-grid-item media-picker disabled" @click="addVariantMedia(index)">
|
||||
<div class="image-holder">
|
||||
<img src="{{ asset('build/assets/placeholder_image.png') }}" class="placeholder-image" alt="Placeholder image">
|
||||
</div>
|
||||
</div>
|
||||
</draggable>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-8">
|
||||
<div class="variant-fields">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label :for="`variants-${variant.uid}-sku`">
|
||||
{{ trans('product::products.form.variants.sku') }}
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
:name="`variants.${variant.uid}.sku`"
|
||||
:id="`variants-${variant.uid}-sku`"
|
||||
class="form-control"
|
||||
v-model="variant.sku"
|
||||
>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`variants.${variant.uid}.sku`)"
|
||||
v-text="errors.get(`variants.${variant.uid}.sku`)"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label :for="`variants-${variant.uid}-price`">
|
||||
{{ trans('product::products.form.variants.price') }}
|
||||
<span class="text-red">*</span>
|
||||
</label>
|
||||
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">$</span>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
:name="`variants.${variant.uid}.price`"
|
||||
min="0"
|
||||
step="0.1"
|
||||
:id="`variants-${variant.uid}-price`"
|
||||
class="form-control"
|
||||
@wheel="$event.target.blur()"
|
||||
v-model.number="variant.price"
|
||||
>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`variants.${variant.uid}.price`)"
|
||||
v-text="errors.get(`variants.${variant.uid}.price`)"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label :for="`variants-${variant.uid}-special-price`">
|
||||
{{ trans('product::products.form.variants.special_price') }}
|
||||
</label>
|
||||
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">
|
||||
@{{ variant.special_price_type === 'fixed' ? '$' : '%' }}
|
||||
</span>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
:name="`variants.${variant.uid}.special_price`"
|
||||
min="0"
|
||||
step="0.1"
|
||||
:id="`variants-${variant.uid}-special-price`"
|
||||
class="form-control"
|
||||
@wheel="$event.target.blur()"
|
||||
v-model="variant.special_price"
|
||||
>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`variants.${variant.uid}.special_price`)"
|
||||
v-text="errors.get(`variants.${variant.uid}.special_price`)"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label :for="`variants-${variant.uid}-special-price-type`">
|
||||
{{ trans('product::products.form.variants.special_price_type') }}
|
||||
</label>
|
||||
|
||||
<select
|
||||
:name="`variants.${variant.uid}.special_price_type`"
|
||||
:id="`variants-${variant.uid}-special-price-type`"
|
||||
class="form-control custom-select-black"
|
||||
v-model="variant.special_price_type"
|
||||
>
|
||||
<option value="fixed">
|
||||
{{ trans('product::products.form.variants.special_price_types.fixed') }}
|
||||
</option>
|
||||
|
||||
<option value="percent">
|
||||
{{ trans('product::products.form.variants.special_price_types.percent') }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`variants.${variant.uid}.special_price_type`)"
|
||||
v-text="errors.get(`variants.${variant.uid}.special_price_type`)"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label :for="`variants-${variant.uid}-special-price-start`">
|
||||
{{ trans('product::products.form.variants.special_price_start') }}
|
||||
</label>
|
||||
|
||||
<flat-pickr
|
||||
:name="`variants.${variant.uid}.special_price_start`"
|
||||
:id="`variants-${variant.uid}-special-price-start`"
|
||||
class="form-control"
|
||||
:config="flatPickrConfig"
|
||||
v-model="variant.special_price_start"
|
||||
>
|
||||
</flat-pickr>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`variants.${variant.uid}.special_price_start`)"
|
||||
v-text="errors.get(`variants.${variant.uid}.special_price_start`)"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label :for="`variants-${variant.uid}-special-price-end`">
|
||||
{{ trans('product::products.form.variants.special_price_end') }}
|
||||
</label>
|
||||
|
||||
<flat-pickr
|
||||
:name="`variants.${variant.uid}.special_price_end`"
|
||||
:id="`variants-${variant.uid}-special-price-end`"
|
||||
class="form-control"
|
||||
:config="flatPickrConfig"
|
||||
v-model="variant.special_price_end"
|
||||
>
|
||||
</flat-pickr>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`variants.${variant.uid}.special_price_end`)"
|
||||
v-text="errors.get(`variants.${variant.uid}.special_price_end`)"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label :for="`variants-${variant.uid}-manage-stock`">
|
||||
{{ trans('product::products.form.variants.manage_stock') }}
|
||||
</label>
|
||||
|
||||
<select
|
||||
:name="`variants.${variant.uid}.manage_stock`"
|
||||
:id="`variants-${variant.uid}-manage-stock`"
|
||||
class="form-control custom-select-black"
|
||||
@change="focusField({
|
||||
selector: `#variants-${variant.uid}-qty`,
|
||||
key: `variants.${variant.uid}.qty`
|
||||
})"
|
||||
v-model.number="variant.manage_stock"
|
||||
>
|
||||
<option value="0">
|
||||
{{ trans('product::products.form.variants.manage_stock_states.0') }}
|
||||
</option>
|
||||
|
||||
<option value="1">
|
||||
{{ trans('product::products.form.variants.manage_stock_states.1') }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`variants.${variant.uid}.manage_stock`)"
|
||||
v-text="errors.get(`variants.${variant.uid}.manage_stock`)"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="variant.manage_stock == 1" class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label :for="`variants-${variant.uid}-qty`">
|
||||
{{ trans('product::products.form.variants.qty') }}<span class="text-red">*</span>
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
:name="`variants.${variant.uid}.qty`"
|
||||
min="0"
|
||||
step="1"
|
||||
:id="`variants-${variant.uid}-qty`"
|
||||
class="form-control"
|
||||
@wheel="$event.target.blur()"
|
||||
v-model="variant.qty"
|
||||
>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`variants.${variant.uid}.qty`)"
|
||||
v-text="errors.get(`variants.${variant.uid}.qty`)"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label :for="`variants-${variant.uid}-in-stock`">
|
||||
{{ trans('product::products.form.variants.in_stock') }}
|
||||
</label>
|
||||
|
||||
<select
|
||||
:name="`variants.${variant.uid}.in_stock`"
|
||||
:id="`variants-${variant.uid}-in-stock`"
|
||||
class="form-control custom-select-black"
|
||||
v-model="variant.in_stock"
|
||||
>
|
||||
<option value="0">
|
||||
{{ trans('product::products.form.variants.stock_availability_states.0') }}
|
||||
</option>
|
||||
|
||||
<option value="1">
|
||||
{{ trans('product::products.form.variants.stock_availability_states.1') }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`variants.${variant.uid}.in_stock`)"
|
||||
v-text="errors.get(`variants.${variant.uid}.in_stock`)"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,310 @@
|
||||
<template v-else-if="section === 'variations'">
|
||||
<div class="box-header">
|
||||
<h5>{{ trans('product::products.group.variations') }}</h5>
|
||||
|
||||
<div class="d-flex">
|
||||
<span
|
||||
class="toggle-accordion"
|
||||
:class="{ 'collapsed': isCollapsedVariationsAccordion }"
|
||||
data-toggle="tooltip"
|
||||
data-placement="top"
|
||||
:data-original-title="
|
||||
isCollapsedVariationsAccordion ?
|
||||
'{{ trans('product::products.section.expand_all') }}' :
|
||||
'{{ trans('product::products.section.collapse_all') }}'
|
||||
"
|
||||
@click="toggleAccordions({
|
||||
selector: '.variations-group .panel-heading',
|
||||
state: isCollapsedVariationsAccordion,
|
||||
data: form.variations
|
||||
})"
|
||||
>
|
||||
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
||||
</span>
|
||||
|
||||
<div class="drag-handle">
|
||||
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
|
||||
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box-body">
|
||||
<div class="accordion-box-content">
|
||||
<draggable
|
||||
animation="150"
|
||||
class="variations-group"
|
||||
force-fallback="true"
|
||||
handle=".drag-handle"
|
||||
@change="reorderVariations"
|
||||
:list="form.variations"
|
||||
>
|
||||
<div
|
||||
v-for="(variation, index) in form.variations"
|
||||
:id="`variation-${variation.uid}`"
|
||||
class="content-accordion panel-group options-group-wrapper"
|
||||
:class="`variation-${variation.uid}`"
|
||||
:key="index"
|
||||
>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" @click.stop="toggleAccordion($event, variation)">
|
||||
<h4 class="panel-title">
|
||||
<div
|
||||
:aria-expanded="variation.is_open"
|
||||
data-toggle="collapse"
|
||||
data-transition="false"
|
||||
:class="{
|
||||
'collapsed': !variation.is_open,
|
||||
'has-error': hasAnyError({
|
||||
name: 'variations',
|
||||
uid: variation.uid
|
||||
})
|
||||
}"
|
||||
>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="drag-handle">
|
||||
<i class="fa"></i>
|
||||
<i class="fa"></i>
|
||||
</span>
|
||||
|
||||
<span v-text="variation.name || '{{ trans('product::products.variations.new_variation') }}'"></span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="delete-option"
|
||||
@click.stop="deleteVariation(index, variation.uid)"
|
||||
>
|
||||
<i class="fa fa-trash"></i>
|
||||
</span>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="panel-collapse" :class="{ 'collapse': !variation.is_open }">
|
||||
<div class="panel-body">
|
||||
<div class="new-option">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label :for="`variations-${variation.uid}-name`">
|
||||
{{ trans('product::products.form.variations.name') }}
|
||||
<span v-if="variation.name || variation.type" class="text-red">*</span>
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
:name="`variations.${variation.uid}.name`"
|
||||
:id="`variations-${variation.uid}-name`"
|
||||
class="form-control"
|
||||
v-model="variation.name"
|
||||
>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`variations.${variation.uid}.name`)"
|
||||
v-text="errors.get(`variations.${variation.uid}.name`)"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3">
|
||||
<div class="form-group">
|
||||
<label :for="`variations-${variation.uid}-type`">
|
||||
{{ trans('product::products.form.variations.type') }}
|
||||
<span v-if="variation.name || variation.type" class="text-red">*</span>
|
||||
</label>
|
||||
|
||||
<select
|
||||
:name="`variations.${variation.uid}.type`"
|
||||
:id="`variations-${variation.uid}-type`"
|
||||
class="form-control custom-select-black"
|
||||
@change="changeVariationType($event.target.value, index, variation.uid)"
|
||||
v-model="variation.type"
|
||||
>
|
||||
<option value="">
|
||||
{{ trans('product::products.form.variations.variation_types.please_select') }}
|
||||
</option>
|
||||
|
||||
<option value="text">
|
||||
{{ trans('product::products.form.variations.variation_types.text') }}
|
||||
</option>
|
||||
|
||||
<option value="color">
|
||||
{{ trans('product::products.form.variations.variation_types.color') }}
|
||||
</option>
|
||||
|
||||
<option value="image">
|
||||
{{ trans('product::products.form.variations.variation_types.image') }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`variations.${variation.uid}.type`)"
|
||||
v-text="errors.get(`variations.${variation.uid}.type`)"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="variation-values" v-if="variation.type !== ''">
|
||||
<div class="table-responsive">
|
||||
<table
|
||||
class="options table table-bordered table-striped"
|
||||
:class="variation.type !== '' ? `type-${variation.type}` : ''"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
{{ trans('product::products.form.variations.label') }}
|
||||
<span class="text-red">*</span>
|
||||
</th>
|
||||
<th v-if="variation.type === 'color'">
|
||||
{{ trans('product::products.form.variations.color') }}
|
||||
<span class="text-red">*</span>
|
||||
</th>
|
||||
<th v-else-if="variation.type === 'image'">
|
||||
{{ trans('product::products.form.variations.image') }}
|
||||
<span class="text-red">*</span>
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody
|
||||
animation="150"
|
||||
handle=".drag-handle"
|
||||
is="draggable"
|
||||
tag="tbody"
|
||||
@change="reorderVariationValues"
|
||||
:list="variation.values"
|
||||
>
|
||||
<tr class="option-row" v-for="(value, valueIndex) in variation.values" :key="valueIndex">
|
||||
<td class="text-center">
|
||||
<span class="drag-handle">
|
||||
<i class="fa"></i>
|
||||
<i class="fa"></i>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
:name="`variations.${variation.uid}.values.${value.uid}.label`"
|
||||
:id="`variations-${variation.uid}-values-${value.uid}-label`"
|
||||
class="form-control"
|
||||
@input="generateVariants"
|
||||
@keyup.enter="addVariationRowOnPressEnter($event, index, valueIndex)"
|
||||
v-model="value.label"
|
||||
>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`variations.${variation.uid}.values.${value.uid}.label`)"
|
||||
v-text="errors.get(`variations.${variation.uid}.values.${value.uid}.label`)"
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
<td v-if="variation.type === 'color'">
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
:name="`variations.${variation.uid}.values.${value.uid}.color`"
|
||||
:id="`variations-${variation.uid}-values-${value.uid}-color`"
|
||||
class="form-control color-picker"
|
||||
v-model="value.color"
|
||||
>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`variations.${variation.uid}.values.${value.uid}.color`)"
|
||||
v-text="errors.get(`variations.${variation.uid}.values.${value.uid}.color`)"
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
<td v-else-if="variation.type === 'image'">
|
||||
<div class="d-flex">
|
||||
<div
|
||||
class="image-holder"
|
||||
@click="chooseVariationImage(index, variation.uid, valueIndex, value.uid)"
|
||||
>
|
||||
<template v-if="value.image.id">
|
||||
<img :src="value.image.path" alt="variation image">
|
||||
</template>
|
||||
|
||||
<img v-else src="{{ asset('build/assets/placeholder_image.png') }}" class="placeholder-image" alt="Placeholder image">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="help-block text-red"
|
||||
v-if="errors.has(`variations.${variation.uid}.values.${value.uid}.image`)"
|
||||
v-text="errors.get(`variations.${variation.uid}.values.${value.uid}.image`)"
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<button
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="btn btn-default delete-row"
|
||||
@click="deleteVariationRow(index, variation.uid, valueIndex, value.uid)"
|
||||
>
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
@click="addVariationRow(index, variation.uid)"
|
||||
>
|
||||
{{ trans('product::products.variations.add_row') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</draggable>
|
||||
|
||||
<div class="accordion-box-footer">
|
||||
<button type="button" class="btn btn-default" @click="addVariation">
|
||||
{{ trans('product::products.variations.add_variation') }}
|
||||
</button>
|
||||
|
||||
@hasAccess('admin.variations.index')
|
||||
<div class="insert-template">
|
||||
<select class="form-control custom-select-black" v-model="globalVariationId">
|
||||
<option value="">
|
||||
{{ trans('product::products.form.variations.select_template') }}
|
||||
</option>
|
||||
|
||||
@foreach ($globalVariations as $globalVariation)
|
||||
<option value="{{ $globalVariation->id }}">{{ $globalVariation->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
:class="{ 'btn-loading': addingGlobalVariation }"
|
||||
:disabled="isAddGlobalVariationDisabled"
|
||||
@click="addGlobalVariation"
|
||||
>
|
||||
{{ trans('product::products.variations.insert') }}
|
||||
</button>
|
||||
</div>
|
||||
@endHasAccess
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,308 @@
|
||||
<div class="bulk-edit-variants overflow-hidden">
|
||||
<div class="form-group">
|
||||
<label for="variation-values-list" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::products.form.variants.bulk_edit') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<select
|
||||
name="variation_values_list"
|
||||
id="variation-values-list"
|
||||
class="form-control custom-select-black"
|
||||
@change="changeBulkEditVariantsUid($event.target.value)"
|
||||
v-model="bulkEditVariantsUid"
|
||||
>
|
||||
<option value="">{{ trans('admin::admin.form.please_select') }}</option>
|
||||
<option value="all">{{ trans('product::products.form.variants.all_variants') }}</option>
|
||||
|
||||
<template v-for="(variation, index) in form.variations">
|
||||
<template v-for="(value, valueIndex) in variation.values">
|
||||
<option v-if="variation.type !== '' && Boolean(value.label)" :key="value.uid" :value="value.uid">
|
||||
@{{ value.label }}
|
||||
</option>
|
||||
</template>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasBulkEditVariantsUid" class="form-group">
|
||||
<label for="bulk-edit-variants-field-type" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::products.form.variants.field_type') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<select
|
||||
name="bulk_edit_variants_field_type"
|
||||
id="bulk-edit-variants-field-type"
|
||||
class="form-control custom-select-black"
|
||||
@change="changeBulkEditVariantsField($event.target.value)"
|
||||
v-model="bulkEditVariantsField"
|
||||
>
|
||||
<option value="">{{ trans('admin::admin.form.please_select') }}</option>
|
||||
<option value="is_active">{{ trans('product::products.form.variants.is_active') }}</option>
|
||||
<option value="media">{{ trans('product::products.form.variants.media') }}</option>
|
||||
<option value="sku">{{ trans('product::products.form.variants.sku') }}</option>
|
||||
<option value="price">{{ trans('product::products.form.variants.price') }}</option>
|
||||
<option value="special_price">{{ trans('product::products.form.variants.special_price') }}</option>
|
||||
<option value="manage_stock">{{ trans('product::products.form.variants.manage_stock') }}</option>
|
||||
<option value="in_stock">{{ trans('product::products.form.variants.in_stock') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="hasBulkEditVariantsUid && hasBulkEditVariantsField">
|
||||
<div v-if="bulkEditVariantsField === 'is_active'" class="form-group">
|
||||
<label for="bulk-edit-variants-is-active" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::products.form.variants.is_active') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<div class="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk_edit_variants_is_active"
|
||||
id="bulk-edit-variants-is-active"
|
||||
v-model="bulkEditVariants.is_active"
|
||||
>
|
||||
|
||||
<label for="bulk-edit-variants-is-active">
|
||||
{{ trans('product::products.form.variants.enable_the_variants') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="bulkEditVariantsField === 'media'" class="form-group">
|
||||
<label class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::products.form.variants.media') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<draggable
|
||||
animation="200"
|
||||
class="product-media-grid"
|
||||
force-fallback="true"
|
||||
handle=".handle"
|
||||
:move="preventLastSlideDrag"
|
||||
:list="bulkEditVariants.media"
|
||||
>
|
||||
<div class="media-grid-item handle" v-for="(media, index) in bulkEditVariants.media" :key="index">
|
||||
<div class="image-holder">
|
||||
<img :src="media.path" alt="product variants media">
|
||||
|
||||
<button type="button" class="btn remove-image" @click="removeBulkEditVariantsMedia(index)"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="media-grid-item media-picker disabled" @click="addBulkEditVariantsMedia">
|
||||
<div class="image-holder">
|
||||
<img src="{{ asset('build/assets/placeholder_image.png') }}" class="placeholder-image" alt="Placeholder image">
|
||||
</div>
|
||||
</div>
|
||||
</draggable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="bulkEditVariantsField === 'sku'" class="form-group">
|
||||
<label for="bulk-edit-variants-sku" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::products.form.variants.sku') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<input
|
||||
type="text"
|
||||
name="bulk_edit_variants_sku"
|
||||
id="bulk-edit-variants-sku"
|
||||
class="form-control"
|
||||
v-model="bulkEditVariants.sku"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="bulkEditVariantsField === 'price'" class="form-group">
|
||||
<label for="bulk-edit-variants-price" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::products.form.variants.price') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">$</span>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
name="bulk_edit_variants_price"
|
||||
min="0"
|
||||
step="0.1"
|
||||
id="bulk-edit-variants-price"
|
||||
class="form-control"
|
||||
@wheel="$event.target.blur()"
|
||||
v-model.number="bulkEditVariants.price"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else-if="bulkEditVariantsField === 'special_price'">
|
||||
<div class="form-group">
|
||||
<label for="bulk-edit-variants-special-price" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::products.form.variants.special_price') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">
|
||||
@{{ bulkEditVariants.special_price_type === 'fixed' ? '$' : '%' }}
|
||||
</span>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
name="bulk_edit_variants_special_price"
|
||||
min="0"
|
||||
step="0.1"
|
||||
id="bulk-edit-variants-special-price"
|
||||
class="form-control"
|
||||
@wheel="$event.target.blur()"
|
||||
v-model="bulkEditVariants.special_price"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="bulk-edit-variants-special-price-type" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::products.form.variants.special_price_type') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<select
|
||||
name="bulk_edit_variants_special_price_type"
|
||||
id="bulk-edit-variants-special-price-type"
|
||||
class="form-control custom-select-black"
|
||||
v-model="bulkEditVariants.special_price_type"
|
||||
>
|
||||
<option value="fixed">
|
||||
{{ trans('product::products.form.variants.special_price_types.fixed') }}
|
||||
</option>
|
||||
|
||||
<option value="percent">
|
||||
{{ trans('product::products.form.variants.special_price_types.percent') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="bulk-edit-variants-special-price-start" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::products.form.variants.special_price_start') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<flat-pickr
|
||||
name="bulk_edit_variants_special_price_start"
|
||||
id="bulk-edit-variants-special-price-start"
|
||||
class="form-control"
|
||||
:config="flatPickrConfig"
|
||||
v-model="bulkEditVariants.special_price_start"
|
||||
>
|
||||
</flat-pickr>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="bulk-edit-variants-special-price-end" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::products.form.variants.special_price_end') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<flat-pickr
|
||||
name="bulk_edit_variants_special_price_end"
|
||||
id="bulk-edit-variants-special-price-end"
|
||||
class="form-control"
|
||||
:config="flatPickrConfig"
|
||||
v-model="bulkEditVariants.special_price_end"
|
||||
>
|
||||
</flat-pickr>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="bulkEditVariantsField === 'manage_stock'">
|
||||
<div class="form-group">
|
||||
<label for="bulk-edit-variants-manage-stock" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::products.form.variants.manage_stock') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<select
|
||||
name="bulk_edit_variants_manage_stock`"
|
||||
id="bulk-edit-variants-manage-stock"
|
||||
class="form-control custom-select-black"
|
||||
@change="focusField({
|
||||
selector: '#bulk-edit-variants-qty'
|
||||
})"
|
||||
v-model="bulkEditVariants.manage_stock"
|
||||
>
|
||||
<option value="0">
|
||||
{{ trans('product::products.form.variants.manage_stock_states.0') }}
|
||||
</option>
|
||||
|
||||
<option value="1">
|
||||
{{ trans('product::products.form.variants.manage_stock_states.1') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="bulkEditVariants.manage_stock == 1" class="form-group">
|
||||
<label for="bulk-edit-variants-qty" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::products.form.variants.qty') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<input
|
||||
type="number"
|
||||
name="bulk_edit_variants_qty"
|
||||
min="0"
|
||||
step="1"
|
||||
id="bulk-edit-variants-qty"
|
||||
class="form-control"
|
||||
@wheel="$event.target.blur()"
|
||||
v-model.number="bulkEditVariants.qty"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else-if="bulkEditVariantsField === 'in_stock'" class="form-group">
|
||||
<label for="bulk-edit-variants-in-stock`" class="col-sm-3 control-label text-left">
|
||||
{{ trans('product::products.form.variants.in_stock') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<select
|
||||
name="bulk_edit_variants_in_stock`"
|
||||
id="bulk-edit-variants-in-stock`"
|
||||
class="form-control custom-select-black"
|
||||
v-model="bulkEditVariants.in_stock"
|
||||
>
|
||||
<option value="0">
|
||||
{{ trans('product::products.form.variants.stock_availability_states.0') }}
|
||||
</option>
|
||||
|
||||
<option value="1">
|
||||
{{ trans('product::products.form.variants.stock_availability_states.1') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-5 col-sm-offset-3">
|
||||
<button type="button" class="btn btn-default" @click="bulkUpdateVariants">
|
||||
{{ trans('product::products.variants.apply') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -6,7 +6,7 @@
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
<script type="module">
|
||||
keypressAction([
|
||||
{ key: 'b', route: "{{ route('admin.products.index') }}" },
|
||||
]);
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
<th>{{ trans('product::products.table.name') }}</th>
|
||||
<th>{{ trans('product::products.table.price') }}</th>
|
||||
<th>{{ trans('admin::admin.table.status') }}</th>
|
||||
<th data-sort>{{ trans('admin::admin.table.created') }}</th>
|
||||
<th data-sort>{{ trans('admin::admin.table.updated') }}</th>
|
||||
</tr>
|
||||
|
||||
@@ -37,3 +37,9 @@ Route::delete('products/{ids}', [
|
||||
'uses' => 'ProductController@destroy',
|
||||
'middleware' => 'can:admin.products.destroy',
|
||||
]);
|
||||
|
||||
Route::get('products/index/table', [
|
||||
'as' => 'admin.products.table',
|
||||
'uses' => 'ProductController@table',
|
||||
'middleware' => 'can:admin.products.index',
|
||||
]);
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('products', 'ProductController@index')->name('products.index');
|
||||
|
||||
Route::get('products/{slug}', 'ProductController@show')->name('products.show');
|
||||
|
||||
Route::get('suggestions', 'SuggestionController@index')->name('suggestions.index');
|
||||
|
||||
Route::post('products/{id}/price', 'ProductPriceController@show')->name('products.price.show');
|
||||
|
||||
Route::get('suggestions', 'SuggestionController@index')->name('suggestions.index');
|
||||
|
||||
69
Modules/Product/Rules/DistinctProductVariationValueLabel.php
Normal file
69
Modules/Product/Rules/DistinctProductVariationValueLabel.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Rules;
|
||||
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
|
||||
class DistinctProductVariationValueLabel implements Rule
|
||||
{
|
||||
/**
|
||||
* All the data under validation.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $data = [];
|
||||
|
||||
|
||||
/**
|
||||
* Create a new rule instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
if (!request($attribute)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$explodedAttributes = explode(".", $attribute);
|
||||
|
||||
array_pop($explodedAttributes);
|
||||
array_pop($explodedAttributes);
|
||||
|
||||
$attribute = join(".", $explodedAttributes);
|
||||
|
||||
return !(array_count_values(
|
||||
array_filter(
|
||||
array_pluck(
|
||||
array_filter(
|
||||
request($attribute)
|
||||
), 'label'
|
||||
)
|
||||
)
|
||||
)[$value] > 1);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function message(): string
|
||||
{
|
||||
return trans('core::validation.distinct');
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,14 @@ class ChosenProductOptions
|
||||
private $product;
|
||||
private $chosenOptions;
|
||||
|
||||
|
||||
public function __construct(Product $product, array $chosenOptions = [])
|
||||
{
|
||||
$this->product = $product;
|
||||
$this->chosenOptions = array_filter($chosenOptions);
|
||||
}
|
||||
|
||||
|
||||
public function getEntities()
|
||||
{
|
||||
$productOptions = $this->product->options()
|
||||
@@ -31,6 +33,7 @@ class ChosenProductOptions
|
||||
return $this->mergeTextTypeOptions($productOptions);
|
||||
}
|
||||
|
||||
|
||||
private function mergeTextTypeOptions($productOptions)
|
||||
{
|
||||
$filteredOptions = collect($this->chosenOptions)->reject(function ($_, $optionId) use ($productOptions) {
|
||||
|
||||
32
Modules/Product/Services/ChosenProductVariations.php
Normal file
32
Modules/Product/Services/ChosenProductVariations.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Services;
|
||||
|
||||
use Modules\Product\Entities\Product;
|
||||
|
||||
class ChosenProductVariations
|
||||
{
|
||||
private $product;
|
||||
private $chosenVariations;
|
||||
|
||||
|
||||
public function __construct(Product $product, array $chosenVariations = [])
|
||||
{
|
||||
$this->product = $product;
|
||||
$this->chosenVariations = array_filter($chosenVariations);
|
||||
}
|
||||
|
||||
|
||||
public function getEntities()
|
||||
{
|
||||
return $this->product->variations()
|
||||
->with(['values' => function ($query) {
|
||||
$query->whereIn('id', array_flatten($this->chosenVariations));
|
||||
}])
|
||||
->whereIn('id', array_keys($this->chosenVariations))
|
||||
->get()
|
||||
->filter(function ($productVariation) {
|
||||
return $productVariation->values->isNotEmpty();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -18,17 +18,28 @@ class SidebarExtender extends BaseSidebarExtender
|
||||
$item->route('admin.products.index');
|
||||
$item->authorize(
|
||||
$this->auth->hasAnyAccess([
|
||||
'admin.products.create',
|
||||
'admin.products.index',
|
||||
'admin.categories.index',
|
||||
'admin.attributes.index',
|
||||
'admin.attribute_sets.index',
|
||||
'admin.variations.index',
|
||||
'admin.options.index',
|
||||
])
|
||||
);
|
||||
|
||||
$item->item(trans('product::sidebar.catalog'), function (Item $item) {
|
||||
$item->item(trans('product::sidebar.create_product'), function (Item $item) {
|
||||
$item->weight(5);
|
||||
$item->route('admin.products.create');
|
||||
$item->authorize(
|
||||
$this->auth->hasAccess('admin.products.create')
|
||||
);
|
||||
});
|
||||
|
||||
$item->item(trans('product::sidebar.all_products'), function (Item $item) {
|
||||
$item->weight(6);
|
||||
$item->route('admin.products.index');
|
||||
$item->isActiveWhen(route('admin.products.index', null, false));
|
||||
$item->authorize(
|
||||
$this->auth->hasAccess('admin.products.index')
|
||||
);
|
||||
|
||||
24
Modules/Product/Transformers/ProductAttributeResource.php
Normal file
24
Modules/Product/Transformers/ProductAttributeResource.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Transformers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ProductAttributeResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param Request
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'attribute_id' => $this->attribute_id,
|
||||
'values' => $this->values->pluck('id'),
|
||||
];
|
||||
}
|
||||
}
|
||||
57
Modules/Product/Transformers/ProductEditResource.php
Normal file
57
Modules/Product/Transformers/ProductEditResource.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Transformers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\Option\Transformers\OptionResource;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Modules\Variation\Transformers\VariationResource;
|
||||
|
||||
class ProductEditResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param Request $request
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'slug' => $this->slug,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'short_description' => $this->short_description,
|
||||
'brand_id' => $this->brand_id ?? '',
|
||||
'categories' => $this->categories->pluck('id'),
|
||||
'tags' => $this->tags->pluck('id'),
|
||||
'attributes' => ProductAttributeResource::collection($this->attributes),
|
||||
'variations' => VariationResource::collection($this->variations()->orderBy('position')->get()),
|
||||
'options' => OptionResource::collection($this->options),
|
||||
'variants' => ProductVariantResource::collection($this->variants()->withoutGlobalScope('active')->orderBy('position')->get()),
|
||||
'media' => $this->filterFiles(['base_image', 'additional_images'])->get()->map->only('id', 'path'),
|
||||
'price' => $this->price->amount(),
|
||||
'tax_class_id' => $this->tax_class_id ?? '',
|
||||
'manage_stock' => $this->manage_stock,
|
||||
'qty' => $this->qty,
|
||||
'in_stock' => $this->in_stock,
|
||||
'special_price_type' => $this->special_price_type,
|
||||
'new_from' => $this->new_from,
|
||||
'new_to' => $this->new_to,
|
||||
'up_sells' => $this->upSellProducts()->orderByPivot('created_at', 'asc')->pluck('id'),
|
||||
'cross_sells' => $this->crossSellProducts()->orderByPivot('created_at', 'asc')->pluck('id'),
|
||||
'related_products' => $this->relatedProducts()->orderByPivot('created_at', 'asc')->pluck('id'),
|
||||
'special_price_start' => $this->special_price_start,
|
||||
'special_price_end' => $this->special_price_end,
|
||||
'meta' => [
|
||||
'meta_title' => $this->meta->meta_title,
|
||||
'meta_description' => $this->meta->meta_description,
|
||||
],
|
||||
'downloads' => $this->filterFiles('downloads')->get()->map->only('id', 'filename'),
|
||||
'is_virtual' => $this->is_virtual,
|
||||
'is_active' => $this->is_active,
|
||||
];
|
||||
}
|
||||
}
|
||||
37
Modules/Product/Transformers/ProductVariantResource.php
Normal file
37
Modules/Product/Transformers/ProductVariantResource.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Transformers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ProductVariantResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param Request
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'uid' => $this->uid,
|
||||
'uids' => $this->uids,
|
||||
'name' => $this->name,
|
||||
'position' => $this->position,
|
||||
'media' => $this->files->map->only('id', 'path'),
|
||||
'manage_stock' => $this->manage_stock,
|
||||
'qty' => $this->qty,
|
||||
'in_stock' => $this->in_stock,
|
||||
'sku' => $this->sku,
|
||||
'price' => $this->price->amount(),
|
||||
'special_price_type' => $this->special_price_type,
|
||||
'special_price' => $this->special_price?->amount(),
|
||||
'is_active' => $this->is_active,
|
||||
'is_default' => $this->is_default,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Modules\Product\Entities\Product;
|
||||
use Modules\FlashSale\Entities\FlashSale;
|
||||
use Modules\Product\Entities\ProductVariant;
|
||||
|
||||
if (! function_exists('product_price_formatted')) {
|
||||
if (!function_exists('product_price_formatted')) {
|
||||
/**
|
||||
* Get the selling price of the given product.
|
||||
*
|
||||
* @param \Modules\Product\Entities\Product $product
|
||||
* @param \Closure $callback
|
||||
* @param Product $productOrProductVariant
|
||||
* @param Closure|null $callback
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function product_price_formatted($product, $callback = null)
|
||||
function product_price_formatted(Product|ProductVariant $productOrProductVariant, Closure $callback = null): string
|
||||
{
|
||||
if (FlashSale::contains($product)) {
|
||||
$previousPrice = $product->getSellingPrice()->convertToCurrentCurrency()->format();
|
||||
$flashSalePrice = FlashSale::pivot($product)->price->convertToCurrentCurrency()->format();
|
||||
if ($productOrProductVariant instanceof Product && FlashSale::contains($productOrProductVariant)) {
|
||||
$sellingPrice = $productOrProductVariant->hasSpecialPrice() ? $productOrProductVariant->getSpecialPrice() : $productOrProductVariant->price;
|
||||
$previousPrice = $sellingPrice->convertToCurrentCurrency()->format();
|
||||
$flashSalePrice = FlashSale::pivot($productOrProductVariant)->price->convertToCurrentCurrency()->format();
|
||||
|
||||
if (is_callable($callback)) {
|
||||
return $callback($flashSalePrice, $previousPrice);
|
||||
@@ -23,14 +27,14 @@ if (! function_exists('product_price_formatted')) {
|
||||
return "{$flashSalePrice} <span class='previous-price'>{$previousPrice}</span>";
|
||||
}
|
||||
|
||||
$price = $product->price->convertToCurrentCurrency()->format();
|
||||
$specialPrice = $product->getSpecialPrice()->convertToCurrentCurrency()->format();
|
||||
$price = $productOrProductVariant->price->convertToCurrentCurrency()->format();
|
||||
$specialPrice = $productOrProductVariant->getSpecialPrice()->convertToCurrentCurrency()->format();
|
||||
|
||||
if (is_callable($callback)) {
|
||||
return $callback($price, $specialPrice);
|
||||
}
|
||||
|
||||
if (! $product->hasSpecialPrice()) {
|
||||
if (!$productOrProductVariant->hasSpecialPrice()) {
|
||||
return $price;
|
||||
}
|
||||
|
||||
|
||||
12
Modules/Product/package.json
Normal file
12
Modules/Product/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "product-module",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@melloware/coloris": "^0.21.1",
|
||||
"blueimp-md5": "^2.19.0",
|
||||
"tinymce": "5.10.7",
|
||||
"vue-flatpickr-component": "8.1.8",
|
||||
"vue2-selectize": "^1.1.7",
|
||||
"vuedraggable": "^2.24.3"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user