first upload all files
This commit is contained in:
41
Modules/Product/Admin/ProductTable.php
Normal file
41
Modules/Product/Admin/ProductTable.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Admin;
|
||||
|
||||
use Modules\Admin\Ui\AdminTable;
|
||||
use Modules\Product\Entities\Product;
|
||||
|
||||
class ProductTable extends AdminTable
|
||||
{
|
||||
/**
|
||||
* Raw columns that will not be escaped.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $rawColumns = ['price'];
|
||||
|
||||
/**
|
||||
* Make table response for the resource.
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function make()
|
||||
{
|
||||
return $this->newTable()
|
||||
->editColumn('thumbnail', function ($product) {
|
||||
return view('admin::partials.table.image', [
|
||||
'file' => $product->base_image,
|
||||
]);
|
||||
})
|
||||
->editColumn('price', function (Product $product) {
|
||||
return product_price_formatted($product, function ($price, $specialPrice) use ($product) {
|
||||
if ($product->hasSpecialPrice()) {
|
||||
return "<span class='m-r-5'>{$specialPrice}</span>
|
||||
<del class='text-red'>{$price}</del>";
|
||||
}
|
||||
|
||||
return "<span class='m-r-5'>{$price}</span>";
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
144
Modules/Product/Admin/ProductTabs.php
Normal file
144
Modules/Product/Admin/ProductTabs.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Admin;
|
||||
|
||||
use Modules\Admin\Ui\Tab;
|
||||
use Modules\Admin\Ui\Tabs;
|
||||
use Modules\Tag\Entities\Tag;
|
||||
use Modules\Brand\Entities\Brand;
|
||||
use Modules\Tax\Entities\TaxClass;
|
||||
use Modules\Category\Entities\Category;
|
||||
|
||||
class ProductTabs extends Tabs
|
||||
{
|
||||
public function make()
|
||||
{
|
||||
$this->group('basic_information', trans('product::products.tabs.group.basic_information'))
|
||||
->active()
|
||||
->add($this->general())
|
||||
->add($this->price())
|
||||
->add($this->inventory())
|
||||
->add($this->images())
|
||||
->add($this->downloads())
|
||||
->add($this->seo());
|
||||
|
||||
$this->group('advanced_information', trans('product::products.tabs.group.advanced_information'))
|
||||
->add($this->relatedProducts())
|
||||
->add($this->upSells())
|
||||
->add($this->crossSells())
|
||||
->add($this->additional());
|
||||
}
|
||||
|
||||
private function general()
|
||||
{
|
||||
return tap(new Tab('general', trans('product::products.tabs.general')), function (Tab $tab) {
|
||||
$tab->active();
|
||||
$tab->weight(5);
|
||||
$tab->fields(['name', 'description', 'brand_id', 'tax_class_id', 'is_active']);
|
||||
$tab->view('product::admin.products.tabs.general', [
|
||||
'brands' => $this->brands(),
|
||||
'categories' => Category::treeList(),
|
||||
'taxClasses' => $this->taxClasses(),
|
||||
'tags' => Tag::list(),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
private function brands()
|
||||
{
|
||||
return Brand::list()->prepend(trans('admin::admin.form.please_select'), '');
|
||||
}
|
||||
|
||||
private function taxClasses()
|
||||
{
|
||||
return TaxClass::list()->prepend(trans('admin::admin.form.please_select'), '');
|
||||
}
|
||||
|
||||
private function price()
|
||||
{
|
||||
return tap(new Tab('price', trans('product::products.tabs.price')), function (Tab $tab) {
|
||||
$tab->weight(10);
|
||||
|
||||
$tab->fields([
|
||||
'price',
|
||||
'special_price',
|
||||
'special_price_type',
|
||||
'special_price_start',
|
||||
'special_price_end',
|
||||
]);
|
||||
|
||||
$tab->view('product::admin.products.tabs.price');
|
||||
});
|
||||
}
|
||||
|
||||
private function inventory()
|
||||
{
|
||||
return tap(new Tab('inventory', trans('product::products.tabs.inventory')), function (Tab $tab) {
|
||||
$tab->weight(15);
|
||||
$tab->fields(['manage_stock', 'qty', 'in_stock']);
|
||||
$tab->view('product::admin.products.tabs.inventory');
|
||||
});
|
||||
}
|
||||
|
||||
private function images()
|
||||
{
|
||||
if (! auth()->user()->hasAccess('admin.media.index')) {
|
||||
return;
|
||||
}
|
||||
|
||||
return tap(new Tab('images', trans('product::products.tabs.images')), function (Tab $tab) {
|
||||
$tab->weight(20);
|
||||
$tab->view('product::admin.products.tabs.images');
|
||||
});
|
||||
}
|
||||
|
||||
private function downloads()
|
||||
{
|
||||
return tap(new Tab('downloads', trans('product::products.tabs.downloads')), function (Tab $tab) {
|
||||
$tab->weight(22);
|
||||
$tab->view('product::admin.products.tabs.downloads');
|
||||
});
|
||||
}
|
||||
|
||||
private function seo()
|
||||
{
|
||||
return tap(new Tab('seo', trans('product::products.tabs.seo')), function (Tab $tab) {
|
||||
$tab->weight(25);
|
||||
$tab->fields(['slug']);
|
||||
$tab->view('product::admin.products.tabs.seo');
|
||||
});
|
||||
}
|
||||
|
||||
private function relatedProducts()
|
||||
{
|
||||
return tap(new Tab('related_products', trans('product::products.tabs.related_products')), function (Tab $tab) {
|
||||
$tab->weight(40);
|
||||
$tab->view('product::admin.products.tabs.products', ['name' => 'related_products']);
|
||||
});
|
||||
}
|
||||
|
||||
private function upSells()
|
||||
{
|
||||
return tap(new Tab('up_sells', trans('product::products.tabs.up_sells')), function (Tab $tab) {
|
||||
$tab->weight(45);
|
||||
$tab->view('product::admin.products.tabs.products', ['name' => 'up_sells']);
|
||||
});
|
||||
}
|
||||
|
||||
private function crossSells()
|
||||
{
|
||||
return tap(new Tab('cross_sells', trans('product::products.tabs.cross_sells')), function (Tab $tab) {
|
||||
$tab->weight(45);
|
||||
$tab->view('product::admin.products.tabs.products', ['name' => 'cross_sells']);
|
||||
});
|
||||
}
|
||||
|
||||
private function additional()
|
||||
{
|
||||
return tap(new Tab('additional', trans('product::products.tabs.additional')), function (Tab $tab) {
|
||||
$tab->weight(55);
|
||||
$tab->fields(['new_from', 'new_to']);
|
||||
$tab->view('product::admin.products.tabs.additional');
|
||||
});
|
||||
}
|
||||
}
|
||||
22
Modules/Product/Config/assets.php
Normal file
22
Modules/Product/Config/assets.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Define which assets will be available through the asset manager
|
||||
|--------------------------------------------------------------------------
|
||||
| These assets are registered on the asset manager
|
||||
*/
|
||||
'all_assets' => [
|
||||
'admin.product.js' => ['module' => 'product:admin/js/product.js'],
|
||||
'admin.product.css' => ['module' => 'product:admin/css/product.css'],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Define which default assets will always be included in your pages
|
||||
| through the asset pipeline
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'required_assets' => [],
|
||||
];
|
||||
38
Modules/Product/Config/config.php
Normal file
38
Modules/Product/Config/config.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'recently_viewed' => [
|
||||
/*
|
||||
* ---------------------------------------------------------------
|
||||
* formatting
|
||||
* ---------------------------------------------------------------
|
||||
*
|
||||
* the formatting of shopping cart values
|
||||
*/
|
||||
'format_numbers' => env('SHOPPING_FORMAT_VALUES', false),
|
||||
|
||||
'decimals' => env('SHOPPING_DECIMALS', 0),
|
||||
|
||||
'dec_point' => env('SHOPPING_DEC_POINT', '.'),
|
||||
|
||||
'thousands_sep' => env('SHOPPING_THOUSANDS_SEP', ','),
|
||||
|
||||
/*
|
||||
* ---------------------------------------------------------------
|
||||
* persistence
|
||||
* ---------------------------------------------------------------
|
||||
*
|
||||
* the configuration for persisting cart
|
||||
*/
|
||||
'storage' => null,
|
||||
|
||||
/*
|
||||
* ---------------------------------------------------------------
|
||||
* events
|
||||
* ---------------------------------------------------------------
|
||||
*
|
||||
* the configuration for cart events
|
||||
*/
|
||||
'events' => null,
|
||||
],
|
||||
];
|
||||
10
Modules/Product/Config/permissions.php
Normal file
10
Modules/Product/Config/permissions.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'admin.products' => [
|
||||
'index' => 'product::permissions.index',
|
||||
'create' => 'product::permissions.create',
|
||||
'edit' => 'product::permissions.edit',
|
||||
'destroy' => 'product::permissions.destroy',
|
||||
],
|
||||
];
|
||||
16
Modules/Product/Database/Factories/ModelFactory.php
Normal file
16
Modules/Product/Database/Factories/ModelFactory.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
use Modules\Product\Entities\Product;
|
||||
|
||||
$factory->define(Product::class, function (Faker\Generator $faker) {
|
||||
return [
|
||||
'name' => $faker->text(60),
|
||||
'description' => $faker->paragraph(),
|
||||
'price' => $faker->numberBetween(10, 9000),
|
||||
'manage_stock' => false,
|
||||
'in_stock' => $faker->boolean(),
|
||||
'slug' => $faker->slug(),
|
||||
'sku' => $faker->word(),
|
||||
'is_active' => $faker->boolean(),
|
||||
];
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateProductsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('products', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->integer('tax_class_id')->unsigned()->nullable();
|
||||
$table->string('slug')->unique();
|
||||
$table->decimal('price', 18, 4)->unsigned();
|
||||
$table->decimal('special_price', 18, 4)->unsigned()->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')->default(0);
|
||||
$table->integer('qty')->nullable();
|
||||
$table->boolean('in_stock')->default(1);
|
||||
$table->integer('viewed')->unsigned()->default(0);
|
||||
$table->boolean('is_active');
|
||||
$table->datetime('new_from')->nullable();
|
||||
$table->datetime('new_to')->nullable();
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('products');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateProductTranslationsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('product_translations', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->integer('product_id')->unsigned();
|
||||
$table->string('locale');
|
||||
$table->string('name');
|
||||
$table->longText('description');
|
||||
$table->text('short_description')->nullable();
|
||||
|
||||
$table->unique(['product_id', 'locale']);
|
||||
$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
|
||||
});
|
||||
|
||||
DB::statement('ALTER TABLE product_translations ADD FULLTEXT(name)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('product_translations');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateRelatedProductsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('related_products', function (Blueprint $table) {
|
||||
$table->integer('product_id')->unsigned();
|
||||
$table->integer('related_product_id')->unsigned();
|
||||
|
||||
$table->primary(['product_id', 'related_product_id']);
|
||||
$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
|
||||
$table->foreign('related_product_id')->references('id')->on('products')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('related_products');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateUpSellProductsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('up_sell_products', function (Blueprint $table) {
|
||||
$table->integer('product_id')->unsigned();
|
||||
$table->integer('up_sell_product_id')->unsigned();
|
||||
|
||||
$table->primary(['product_id', 'up_sell_product_id']);
|
||||
$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
|
||||
$table->foreign('up_sell_product_id')->references('id')->on('products')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('up_sell_products');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateCrossSellProductsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('cross_sell_products', function (Blueprint $table) {
|
||||
$table->integer('product_id')->unsigned();
|
||||
$table->integer('cross_sell_product_id')->unsigned();
|
||||
|
||||
$table->primary(['product_id', 'cross_sell_product_id']);
|
||||
$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
|
||||
$table->foreign('cross_sell_product_id')->references('id')->on('products')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('cross_sell_products');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateProductCategoriesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('product_categories', function (Blueprint $table) {
|
||||
$table->integer('product_id')->unsigned();
|
||||
$table->integer('category_id')->unsigned();
|
||||
|
||||
$table->primary(['product_id', 'category_id']);
|
||||
$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
|
||||
$table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('product_categories');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateSearchTermsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('search_terms', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('term')->unique();
|
||||
$table->integer('results')->unsigned();
|
||||
$table->integer('hits')->unsigned()->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('search_terms');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddSpecialPriceTypeColumnToProductsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->string('special_price_type')->nullable()->after('special_price');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->dropColumn('special_price_type');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddBrandIdColumnToProductsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->integer('brand_id')->unsigned()->nullable()->after('id');
|
||||
|
||||
$table->foreign('brand_id')->references('id')->on('brands')->onDelete('set null');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->dropForeign(['brand_id']);
|
||||
$table->dropColumn('brand_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateProductTagsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('product_tags', function (Blueprint $table) {
|
||||
$table->integer('product_id')->unsigned();
|
||||
$table->integer('tag_id')->unsigned();
|
||||
|
||||
$table->primary(['product_id', 'tag_id']);
|
||||
$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
|
||||
$table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('product_tags');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddDownloadsColumnsToProductsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->boolean('virtual')->default(false)->before('is_active');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->dropColumn('virtual');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->renameColumn('virtual', 'is_virtual');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->renameColumn('is_virtual', 'virtual');
|
||||
});
|
||||
}
|
||||
};
|
||||
19
Modules/Product/Database/Seeders/ProductDatabaseSeeder.php
Normal file
19
Modules/Product/Database/Seeders/ProductDatabaseSeeder.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Modules\Product\Entities\Product;
|
||||
|
||||
class ProductDatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
factory(Product::class, 20)->create();
|
||||
}
|
||||
}
|
||||
590
Modules/Product/Entities/Product.php
Normal file
590
Modules/Product/Entities/Product.php
Normal file
@@ -0,0 +1,590 @@
|
||||
<?php
|
||||
|
||||
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 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 Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Modules\Attribute\Entities\ProductAttribute;
|
||||
|
||||
class Product extends Model
|
||||
{
|
||||
use Translatable, Searchable, Sluggable, HasMedia, HasMetaData, SoftDeletes;
|
||||
|
||||
/**
|
||||
* The relations to eager load on every query.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $with = ['translations'];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @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'];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to native types.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'manage_stock' => 'boolean',
|
||||
'in_stock' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['special_price_start', 'special_price_end', 'new_from', 'new_to', 'start_date', 'end_date', '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'];
|
||||
|
||||
/**
|
||||
* The attributes that are translatable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $translatedAttributes = ['name', 'description', 'short_description'];
|
||||
|
||||
/**
|
||||
* The attribute that will be slugged.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $slugAttribute = 'name';
|
||||
|
||||
/**
|
||||
* Perform any actions required after the model boots.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected static function booted()
|
||||
{
|
||||
static::saved(function ($product) {
|
||||
if (!empty(request()->all())) {
|
||||
$product->saveRelations(request()->all());
|
||||
}
|
||||
|
||||
$product->withoutEvents(function () use ($product) {
|
||||
$product->update(['selling_price' => $product->getSellingPrice()->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
|
||||
*/
|
||||
public function table($request)
|
||||
{
|
||||
$query = $this->newQuery()
|
||||
->withoutGlobalScope('active')
|
||||
->withName()
|
||||
->withBaseImage()
|
||||
->withPrice()
|
||||
->addSelect(['id', 'is_active', 'created_at'])
|
||||
->when($request->has('except'), function ($query) use ($request) {
|
||||
$query->whereNotIn('id', explode(',', $request->except));
|
||||
});
|
||||
|
||||
return new ProductTable($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save associated relations for the product.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @return void
|
||||
*/
|
||||
public function saveRelations($attributes = [])
|
||||
{
|
||||
$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', []));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the indexable data array for the product.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toSearchableArray()
|
||||
{
|
||||
// MySQL Full-Text search handles indexing automatically.
|
||||
if (config('scout.driver') === 'mysql') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$translations = $this->translations()
|
||||
->withoutGlobalScope('locale')
|
||||
->get(['name', 'description', 'short_description']);
|
||||
|
||||
return ['id' => $this->id, 'translations' => $translations];
|
||||
}
|
||||
|
||||
public function searchTable()
|
||||
{
|
||||
return 'product_translations';
|
||||
}
|
||||
|
||||
public function searchKey()
|
||||
{
|
||||
return 'product_id';
|
||||
}
|
||||
|
||||
public function searchColumns()
|
||||
{
|
||||
return ['name'];
|
||||
}
|
||||
|
||||
public function searchExactColumn()
|
||||
{
|
||||
return 'name';
|
||||
}
|
||||
}
|
||||
15
Modules/Product/Entities/ProductTranslation.php
Normal file
15
Modules/Product/Entities/ProductTranslation.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Entities;
|
||||
|
||||
use Modules\Support\Eloquent\TranslationModel;
|
||||
|
||||
class ProductTranslation extends TranslationModel
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = ['name', 'description', 'short_description'];
|
||||
}
|
||||
15
Modules/Product/Entities/SearchTerm.php
Normal file
15
Modules/Product/Entities/SearchTerm.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Entities;
|
||||
|
||||
use Modules\Support\Eloquent\Model;
|
||||
|
||||
class SearchTerm extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that aren't mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
27
Modules/Product/Events/ProductViewed.php
Normal file
27
Modules/Product/Events/ProductViewed.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Events;
|
||||
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ProductViewed
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* The product entity.
|
||||
*
|
||||
* @var \Modules\Product\Entities\Product
|
||||
*/
|
||||
public $product;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($product)
|
||||
{
|
||||
$this->product = $product;
|
||||
}
|
||||
}
|
||||
27
Modules/Product/Events/ShowingProductList.php
Normal file
27
Modules/Product/Events/ShowingProductList.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Events;
|
||||
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ShowingProductList
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Collection of product.
|
||||
*
|
||||
* @var \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public $products;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($products)
|
||||
{
|
||||
$this->products = $products;
|
||||
}
|
||||
}
|
||||
43
Modules/Product/Filters/ProductFilter.php
Normal file
43
Modules/Product/Filters/ProductFilter.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Filters;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
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)) {
|
||||
$this->queryStringFilter->{$name}($query, $value);
|
||||
}
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
private function filters()
|
||||
{
|
||||
return array_filter($this->request->query(), function ($filter) {
|
||||
return $this->filterExists($filter);
|
||||
}, ARRAY_FILTER_USE_KEY);
|
||||
}
|
||||
|
||||
private function filterExists($filter)
|
||||
{
|
||||
return method_exists($this->queryStringFilter, $filter) &&
|
||||
is_callable([$this->queryStringFilter, $filter]);
|
||||
}
|
||||
}
|
||||
152
Modules/Product/Filters/QueryStringFilter.php
Normal file
152
Modules/Product/Filters/QueryStringFilter.php
Normal file
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Filters;
|
||||
|
||||
use Modules\Support\Money;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Modules\Attribute\Entities\Attribute;
|
||||
use Modules\Attribute\Entities\AttributeValue;
|
||||
|
||||
class QueryStringFilter
|
||||
{
|
||||
private $sorts = [
|
||||
'relevance',
|
||||
'alphabetic',
|
||||
'toprated',
|
||||
'latest',
|
||||
'pricelowtohigh',
|
||||
'pricehightolow',
|
||||
];
|
||||
|
||||
private $groupColumns = [
|
||||
'products.id',
|
||||
'slug',
|
||||
'price',
|
||||
'selling_price',
|
||||
'special_price',
|
||||
'special_price_type',
|
||||
'special_price_start',
|
||||
'special_price_end',
|
||||
'in_stock',
|
||||
'manage_stock',
|
||||
'qty',
|
||||
'new_from',
|
||||
'new_to',
|
||||
];
|
||||
|
||||
public function sort($query, $sortType)
|
||||
{
|
||||
if ($this->sortTypeExists($sortType)) {
|
||||
return $this->{$sortType}($query);
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
public function topRated($query)
|
||||
{
|
||||
$query->selectRaw('AVG(reviews.rating) as avg_rating')
|
||||
->leftJoin('reviews', function (JoinClause $join) {
|
||||
$join->on('products.id', '=', 'reviews.product_id');
|
||||
$join->on('reviews.is_approved', '=', DB::raw('1'));
|
||||
})
|
||||
->groupBy($this->groupColumns)
|
||||
->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));
|
||||
}
|
||||
|
||||
public function toPrice($query, $price)
|
||||
{
|
||||
$query->where('selling_price', '<=', $this->convertPrice($price));
|
||||
}
|
||||
|
||||
private function convertPrice($price)
|
||||
{
|
||||
return Money::inCurrentCurrency($price)->convertToDefaultCurrency()->amount();
|
||||
}
|
||||
|
||||
public function brand($query, $slug)
|
||||
{
|
||||
$query->whereHas('brand', function ($brandQuery) use ($slug) {
|
||||
$brandQuery->where('slug', $slug);
|
||||
});
|
||||
}
|
||||
|
||||
public function category($query, $slug)
|
||||
{
|
||||
$query->whereHas('categories', function ($categoryQuery) use ($slug) {
|
||||
$categoryQuery->where('slug', $slug);
|
||||
});
|
||||
}
|
||||
|
||||
public function tag($query, $slug)
|
||||
{
|
||||
$query->whereHas('tags', function ($tagQuery) use ($slug) {
|
||||
$tagQuery->where('slug', $slug);
|
||||
});
|
||||
}
|
||||
|
||||
public function attribute($query, $attributeFilters)
|
||||
{
|
||||
foreach ($this->getAttributeIds($attributeFilters) as $index => $attributeId) {
|
||||
$query->join("product_attributes as pa_{$index}", 'products.id', '=', "pa_{$index}.product_id")
|
||||
->whereRaw("pa_{$index}.attribute_id = {$attributeId} AND EXISTS (
|
||||
SELECT *
|
||||
FROM `product_attribute_values`
|
||||
WHERE `pa_{$index}`.`id` = `product_attribute_values`.`product_attribute_id`
|
||||
AND `attribute_value_id` in ({$this->getAttributeValueIds($attributeFilters)})
|
||||
)");
|
||||
}
|
||||
}
|
||||
|
||||
private function getAttributeIds($attributeFilters)
|
||||
{
|
||||
return Attribute::whereIn('slug', array_keys($attributeFilters))->pluck('id');
|
||||
}
|
||||
|
||||
private function getAttributeValueIds($attributeFilters)
|
||||
{
|
||||
return once(function () use ($attributeFilters) {
|
||||
return AttributeValue::whereTranslationIn('value', array_flatten($attributeFilters))
|
||||
->pluck('id')
|
||||
->implode(',') ?: 'null';
|
||||
});
|
||||
}
|
||||
}
|
||||
40
Modules/Product/Http/Controllers/Admin/ProductController.php
Normal file
40
Modules/Product/Http/Controllers/Admin/ProductController.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Http\Controllers\Admin;
|
||||
|
||||
use Modules\Product\Entities\Product;
|
||||
use Modules\Admin\Traits\HasCrudActions;
|
||||
use Modules\Product\Http\Requests\SaveProductRequest;
|
||||
|
||||
class ProductController
|
||||
{
|
||||
use HasCrudActions;
|
||||
|
||||
/**
|
||||
* Model for the resource.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $model = Product::class;
|
||||
|
||||
/**
|
||||
* Label of the resource.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $label = 'product::products.product';
|
||||
|
||||
/**
|
||||
* View path of the resource.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $viewPath = 'product::admin.products';
|
||||
|
||||
/**
|
||||
* Form requests for the resource.
|
||||
*
|
||||
* @var array|string
|
||||
*/
|
||||
protected $validation = SaveProductRequest::class;
|
||||
}
|
||||
68
Modules/Product/Http/Controllers/ProductController.php
Normal file
68
Modules/Product/Http/Controllers/ProductController.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Http\Controllers;
|
||||
|
||||
use Illuminate\Routing\Controller;
|
||||
use Modules\Review\Entities\Review;
|
||||
use Modules\Product\Entities\Product;
|
||||
use Modules\Product\Events\ProductViewed;
|
||||
use Modules\Product\Filters\ProductFilter;
|
||||
use Modules\Product\Http\Middleware\SetProductSortOption;
|
||||
|
||||
class ProductController extends Controller
|
||||
{
|
||||
use ProductSearch;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$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
|
||||
*/
|
||||
public function index(Product $model, ProductFilter $productFilter)
|
||||
{
|
||||
if (request()->expectsJson()) {
|
||||
return $this->searchProducts($model, $productFilter);
|
||||
}
|
||||
|
||||
return view('public.products.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the specified resource.
|
||||
*
|
||||
* @param string $slug
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function show($slug)
|
||||
{
|
||||
$product = Product::findBySlug($slug);
|
||||
$relatedProducts = $product->relatedProducts()->forCard()->get();
|
||||
$upSellProducts = $product->upSellProducts()->forCard()->get();
|
||||
$review = $this->getReviewData($product);
|
||||
|
||||
event(new ProductViewed($product));
|
||||
|
||||
return view('public.products.show', compact('product', 'relatedProducts', 'upSellProducts', 'review'));
|
||||
}
|
||||
|
||||
private function getReviewData(Product $product)
|
||||
{
|
||||
if (! setting('reviews_enabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Review::countAndAvgRating($product);
|
||||
}
|
||||
}
|
||||
52
Modules/Product/Http/Controllers/ProductPriceController.php
Normal file
52
Modules/Product/Http/Controllers/ProductPriceController.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Http\Controllers;
|
||||
|
||||
use Modules\Cart\CartItem;
|
||||
use Darryldecode\Cart\ItemCollection;
|
||||
use Modules\Product\Entities\Product;
|
||||
use Modules\Product\Services\ChosenProductOptions;
|
||||
|
||||
class ProductPriceController
|
||||
{
|
||||
/**
|
||||
* Show the specified resource.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function show($id)
|
||||
{
|
||||
$product = Product::queryWithoutEagerRelations()
|
||||
->select('id')
|
||||
->withPrice()
|
||||
->findOrFail($id);
|
||||
|
||||
$variantPrice = $this->cartItem($product, request('options', []))
|
||||
->total()
|
||||
->convertToCurrentCurrency()
|
||||
->format();
|
||||
|
||||
return product_price_formatted($product, function ($price) use ($product, $variantPrice) {
|
||||
if (! $product->hasSpecialPrice()) {
|
||||
return $variantPrice;
|
||||
}
|
||||
|
||||
return "{$variantPrice} <span class='previous-price'>{$price}</span>";
|
||||
});
|
||||
}
|
||||
|
||||
private function cartItem(Product $product, array $options)
|
||||
{
|
||||
$chosenOptions = new ChosenProductOptions($product, $options);
|
||||
|
||||
return new CartItem(new ItemCollection([
|
||||
'id' => $product->id,
|
||||
'quantity' => 1,
|
||||
'attributes' => [
|
||||
'product' => $product,
|
||||
'options' => $chosenOptions->getEntities(),
|
||||
],
|
||||
]));
|
||||
}
|
||||
}
|
||||
74
Modules/Product/Http/Controllers/ProductSearch.php
Normal file
74
Modules/Product/Http/Controllers/ProductSearch.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Http\Controllers;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Modules\Product\Entities\Product;
|
||||
use Modules\Category\Entities\Category;
|
||||
use Modules\Attribute\Entities\Attribute;
|
||||
use Modules\Product\Filters\ProductFilter;
|
||||
use Modules\Product\Events\ShowingProductList;
|
||||
|
||||
trait ProductSearch
|
||||
{
|
||||
/**
|
||||
* Search products for the request.
|
||||
*
|
||||
* @param \Modules\Product\Entities\Product $model
|
||||
* @param \Modules\Product\Filters\ProductFilter $productFilter
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function searchProducts(Product $model, ProductFilter $productFilter)
|
||||
{
|
||||
$productIds = [];
|
||||
|
||||
if (request()->filled('query')) {
|
||||
$model = $model->search(request('query'));
|
||||
$productIds = $model->keys();
|
||||
}
|
||||
|
||||
$query = $model->filter($productFilter);
|
||||
|
||||
if (request()->filled('category')) {
|
||||
$productIds = (clone $query)->select('products.id')->resetOrders()->pluck('id');
|
||||
}
|
||||
|
||||
$products = $query->paginate(request('perPage', 30));
|
||||
|
||||
event(new ShowingProductList($products));
|
||||
|
||||
return response()->json([
|
||||
'products' => $products,
|
||||
'attributes' => $this->getAttributes($productIds),
|
||||
]);
|
||||
}
|
||||
|
||||
private function getAttributes($productIds)
|
||||
{
|
||||
if (! request()->filled('category') || $this->filteringViaRootCategory()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return Attribute::with('values')
|
||||
->where('is_filterable', true)
|
||||
->whereHas('categories', function ($query) use ($productIds) {
|
||||
$query->whereIn('id', $this->getProductsCategoryIds($productIds));
|
||||
})
|
||||
->get();
|
||||
}
|
||||
|
||||
private function filteringViaRootCategory()
|
||||
{
|
||||
return Category::where('slug', request('category'))
|
||||
->firstOrNew([])
|
||||
->isRoot();
|
||||
}
|
||||
|
||||
private function getProductsCategoryIds($productIds)
|
||||
{
|
||||
return DB::table('product_categories')
|
||||
->whereIn('product_id', $productIds)
|
||||
->distinct()
|
||||
->pluck('category_id');
|
||||
}
|
||||
}
|
||||
83
Modules/Product/Http/Controllers/SuggestionController.php
Normal file
83
Modules/Product/Http/Controllers/SuggestionController.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Http\Controllers;
|
||||
|
||||
use Modules\Product\Entities\Product;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Modules\Product\Http\Response\SuggestionsResponse;
|
||||
|
||||
class SuggestionController
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function index(Product $model)
|
||||
{
|
||||
$products = $this->getProducts($model);
|
||||
|
||||
return new SuggestionsResponse(
|
||||
request('query'),
|
||||
$products,
|
||||
$products->pluck('categories')->flatten(),
|
||||
$this->getTotalResults($model)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private function getProducts(Product $model)
|
||||
{
|
||||
return $model->search(request('query'))
|
||||
->query()
|
||||
->limit(10)
|
||||
->withName()
|
||||
->withBaseImage()
|
||||
->withPrice()
|
||||
->addSelect([
|
||||
'products.id',
|
||||
'products.slug',
|
||||
'products.in_stock',
|
||||
'products.manage_stock',
|
||||
'products.qty',
|
||||
])
|
||||
->with(['files', 'categories' => function ($query) {
|
||||
$query->limit(5);
|
||||
}])
|
||||
->when(request()->filled('category'), $this->categoryQuery())
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns categories condition closure.
|
||||
*
|
||||
* @return \Closure
|
||||
*/
|
||||
private function categoryQuery()
|
||||
{
|
||||
return function (Builder $query) {
|
||||
$query->whereHas('categories', function ($categoryQuery) {
|
||||
$categoryQuery->where('slug', request('category'));
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
51
Modules/Product/Http/Middleware/SetProductSortOption.php
Normal file
51
Modules/Product/Http/Middleware/SetProductSortOption.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SetProductSortOption
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if ($this->shouldSetRelevanceSortOption($request)) {
|
||||
$request->query->set('sort', 'relevance');
|
||||
}
|
||||
|
||||
if ($this->shouldSetLatestSortOption($request)) {
|
||||
$request->query->set('sort', 'latest');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the request should set "relevance" sort option.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return void
|
||||
*/
|
||||
private function shouldSetRelevanceSortOption($request)
|
||||
{
|
||||
return $request->has('query') && ! $request->has('sort');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the request should set "latest" sort option.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return bool
|
||||
*/
|
||||
private function shouldSetLatestSortOption($request)
|
||||
{
|
||||
return ! $request->has('query') && ! $request->has('sort');
|
||||
}
|
||||
}
|
||||
58
Modules/Product/Http/Requests/SaveProductRequest.php
Normal file
58
Modules/Product/Http/Requests/SaveProductRequest.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Http\Requests;
|
||||
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\Product\Entities\Product;
|
||||
use Modules\Core\Http\Requests\Request;
|
||||
|
||||
class SaveProductRequest extends Request
|
||||
{
|
||||
/**
|
||||
* Available attributes.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $availableAttributes = 'product::attributes';
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
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',
|
||||
];
|
||||
}
|
||||
|
||||
private function getSlugRules()
|
||||
{
|
||||
$rules = $this->route()->getName() === 'admin.products.update' ? ['required'] : ['sometimes'];
|
||||
|
||||
$slug = Product::withoutGlobalScope('active')
|
||||
->where('id', $this->id)
|
||||
->value('slug');
|
||||
|
||||
$rules[] = Rule::unique('products', 'slug')->ignore($slug, 'slug');
|
||||
|
||||
return $rules;
|
||||
}
|
||||
}
|
||||
105
Modules/Product/Http/Response/SuggestionsResponse.php
Normal file
105
Modules/Product/Http/Response/SuggestionsResponse.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Http\Response;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Modules\Product\Entities\Product;
|
||||
use Modules\Category\Entities\Category;
|
||||
use Illuminate\Contracts\Support\Responsable;
|
||||
|
||||
class SuggestionsResponse implements Responsable
|
||||
{
|
||||
private $query;
|
||||
private $products;
|
||||
private $categories;
|
||||
private $totalResults;
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
*
|
||||
* @param string $query
|
||||
* @param int $totalResults
|
||||
* @param \Illuminate\Support\Collection $products
|
||||
* @param \Illuminate\Support\Collection $categories
|
||||
*/
|
||||
public function __construct($query, Collection $products, Collection $categories, $totalResults)
|
||||
{
|
||||
$this->query = $query;
|
||||
$this->products = $products;
|
||||
$this->categories = $categories;
|
||||
$this->totalResults = $totalResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTP response that represents the object.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function toResponse($request)
|
||||
{
|
||||
return response()->json([
|
||||
'categories' => $this->transformCategories(),
|
||||
'products' => $this->transformProducts(),
|
||||
'remaining' => $this->getRemainingCount(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the categories.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
private function transformCategories()
|
||||
{
|
||||
return $this->categories->map(function (Category $category) {
|
||||
return [
|
||||
'slug' => $category->slug,
|
||||
'name' => $category->name,
|
||||
'url' => $category->url(),
|
||||
];
|
||||
})->unique('slug')->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the products.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
private function transformProducts()
|
||||
{
|
||||
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(),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight the given text.
|
||||
*
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
private function highlight($text)
|
||||
{
|
||||
$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()
|
||||
{
|
||||
return $this->totalResults - $this->products->count();
|
||||
}
|
||||
}
|
||||
42
Modules/Product/Listeners/AddToRecentlyViewed.php
Normal file
42
Modules/Product/Listeners/AddToRecentlyViewed.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Listeners;
|
||||
|
||||
use Exception;
|
||||
use Modules\Product\RecentlyViewed;
|
||||
use Modules\Product\Events\ProductViewed;
|
||||
|
||||
class AddToRecentlyViewed
|
||||
{
|
||||
/**
|
||||
* The recently viewed instance.
|
||||
*
|
||||
* @var \Modules\Product\RecentlyViewed
|
||||
*/
|
||||
private $recentlyViewed;
|
||||
|
||||
/**
|
||||
* Create a new event listener.
|
||||
*
|
||||
* @param \Modules\Product\RecentlyViewed $recentlyViewed
|
||||
*/
|
||||
public function __construct(RecentlyViewed $recentlyViewed)
|
||||
{
|
||||
$this->recentlyViewed = $recentlyViewed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param \Modules\Product\Events\ProductViewed $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle(ProductViewed $event)
|
||||
{
|
||||
try {
|
||||
$this->recentlyViewed->store($event->product);
|
||||
} catch (Exception $e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Modules/Product/Listeners/IncrementProductView.php
Normal file
19
Modules/Product/Listeners/IncrementProductView.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Listeners;
|
||||
|
||||
use Modules\Product\Events\ProductViewed;
|
||||
|
||||
class IncrementProductView
|
||||
{
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param \Modules\Product\Events\ProductViewed $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle(ProductViewed $event)
|
||||
{
|
||||
$event->product->increment('viewed');
|
||||
}
|
||||
}
|
||||
27
Modules/Product/Listeners/StoreSearchTerm.php
Normal file
27
Modules/Product/Listeners/StoreSearchTerm.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Listeners;
|
||||
|
||||
use Modules\Product\Entities\SearchTerm;
|
||||
use Modules\Product\Events\ShowingProductList;
|
||||
|
||||
class StoreSearchTerm
|
||||
{
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param \Modules\Product\Events\ShowingProductList $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle(ShowingProductList $event)
|
||||
{
|
||||
if (! request()->filled('query')) {
|
||||
return;
|
||||
}
|
||||
|
||||
SearchTerm::updateOrCreate(
|
||||
['term' => request('query')],
|
||||
['results' => $event->products->count()]
|
||||
)->increment('hits');
|
||||
}
|
||||
}
|
||||
23
Modules/Product/Providers/EventServiceProvider.php
Normal file
23
Modules/Product/Providers/EventServiceProvider.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The event listener mappings for the application.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $listen = [
|
||||
\Modules\Product\Events\ProductViewed::class => [
|
||||
\Modules\Product\Listeners\IncrementProductView::class,
|
||||
\Modules\Product\Listeners\AddToRecentlyViewed::class,
|
||||
],
|
||||
\Modules\Product\Events\ShowingProductList::class => [
|
||||
\Modules\Product\Listeners\StoreSearchTerm::class,
|
||||
],
|
||||
];
|
||||
}
|
||||
48
Modules/Product/Providers/ProductServiceProvider.php
Normal file
48
Modules/Product/Providers/ProductServiceProvider.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Providers;
|
||||
|
||||
use Modules\Product\RecentlyViewed;
|
||||
use Modules\Support\Traits\AddsAsset;
|
||||
use Modules\Product\Admin\ProductTabs;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\Admin\Ui\Facades\TabManager;
|
||||
|
||||
class ProductServiceProvider extends ServiceProvider
|
||||
{
|
||||
use AddsAsset;
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
TabManager::register('products', ProductTabs::class);
|
||||
|
||||
$this->addAdminAssets('admin.products.(create|edit)', [
|
||||
'admin.media.css', 'admin.media.js', 'admin.product.css', 'admin.product.js',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the service provider.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->singleton(RecentlyViewed::class, function ($app) {
|
||||
return new RecentlyViewed(
|
||||
$app['session'],
|
||||
$app['events'],
|
||||
'recently_viewed',
|
||||
session()->getId() . '_recently_viewed',
|
||||
config('fleetcart.modules.product.config.recently_viewed')
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->alias(RecentlyViewed::class, 'recently_viewed');
|
||||
}
|
||||
}
|
||||
30
Modules/Product/RecentlyViewed.php
Normal file
30
Modules/Product/RecentlyViewed.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product;
|
||||
|
||||
use Darryldecode\Cart\Cart as DarryldecodeCart;
|
||||
|
||||
class RecentlyViewed extends DarryldecodeCart
|
||||
{
|
||||
public function store($product)
|
||||
{
|
||||
$product->load('reviews');
|
||||
|
||||
$this->remove($product->id);
|
||||
|
||||
return $this->add([
|
||||
'id' => $product->id,
|
||||
'name' => $product->name,
|
||||
'price' => $product->selling_price->amount(),
|
||||
'quantity' => 1,
|
||||
'attributes' => compact('product'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function products()
|
||||
{
|
||||
return $this->getContent()->map(function ($item) {
|
||||
return $item->attributes->product;
|
||||
});
|
||||
}
|
||||
}
|
||||
32
Modules/Product/Resources/assets/admin/js/Download.js
Normal file
32
Modules/Product/Resources/assets/admin/js/Download.js
Normal file
@@ -0,0 +1,32 @@
|
||||
export default class {
|
||||
constructor(download) {
|
||||
this.downloadHtml = this.getDownloadHtml(download);
|
||||
}
|
||||
|
||||
getDownloadHtml(download) {
|
||||
let template = _.template($('#product-download-template').html());
|
||||
|
||||
return $(template(download));
|
||||
}
|
||||
|
||||
render() {
|
||||
this.attachEventListeners();
|
||||
|
||||
return this.downloadHtml;
|
||||
}
|
||||
|
||||
attachEventListeners() {
|
||||
this.downloadHtml.find('.delete-row').on('click', () => {
|
||||
this.downloadHtml.remove();
|
||||
});
|
||||
|
||||
this.downloadHtml.find('.btn-choose-file').on('click', () => {
|
||||
let picker = new MediaPicker();
|
||||
|
||||
picker.on('select', (file) => {
|
||||
this.downloadHtml.find('.download-name').val(file.filename);
|
||||
this.downloadHtml.find('.download-file').val(file.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
44
Modules/Product/Resources/assets/admin/js/Downloads.js
Normal file
44
Modules/Product/Resources/assets/admin/js/Downloads.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import Download from './Download';
|
||||
|
||||
export default class {
|
||||
constructor() {
|
||||
this.downloadsCount = 0;
|
||||
|
||||
this.addDownloads(FleetCart.data['product.downloads']);
|
||||
|
||||
if (this.downloadsCount === 0) {
|
||||
this.addDownload();
|
||||
}
|
||||
|
||||
this.attachEventListeners();
|
||||
this.makeDownloadsSortable();
|
||||
}
|
||||
|
||||
addDownloads(downloads) {
|
||||
for (let attributes of downloads) {
|
||||
this.addDownload(attributes);
|
||||
}
|
||||
}
|
||||
|
||||
addDownload(attributes = {}) {
|
||||
let download = new Download({ download: attributes });
|
||||
|
||||
$('#downloads-wrapper').append(download.render());
|
||||
|
||||
this.downloadsCount++;
|
||||
window.admin.tooltip();
|
||||
}
|
||||
|
||||
attachEventListeners() {
|
||||
$('#add-new-file').on('click', () => {
|
||||
this.addDownload();
|
||||
});
|
||||
}
|
||||
|
||||
makeDownloadsSortable() {
|
||||
Sortable.create(document.getElementById('downloads-wrapper'), {
|
||||
handle: '.drag-icon',
|
||||
animation: 150,
|
||||
});
|
||||
}
|
||||
}
|
||||
34
Modules/Product/Resources/assets/admin/js/ProductForm.js
Normal file
34
Modules/Product/Resources/assets/admin/js/ProductForm.js
Normal file
@@ -0,0 +1,34 @@
|
||||
export default class {
|
||||
constructor() {
|
||||
this.managerStock();
|
||||
|
||||
window.admin.removeSubmitButtonOffsetOn([
|
||||
'#images', '#downloads', '#attributes', '#options',
|
||||
'#related_products', '#up_sells', '#cross_sells', '#reviews',
|
||||
]);
|
||||
|
||||
$('#product-create-form, #product-edit-form').on('submit', this.submit);
|
||||
}
|
||||
|
||||
managerStock() {
|
||||
$('#manage_stock').on('change', (e) => {
|
||||
if (e.currentTarget.value === '1') {
|
||||
$('#qty-field').removeClass('hide');
|
||||
} else {
|
||||
$('#qty-field').addClass('hide');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
submit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
DataTable.removeLengthFields();
|
||||
|
||||
window.form.appendHiddenInputs('#product-create-form, #product-edit-form', 'up_sells', DataTable.getSelectedIds('#up_sells .table'));
|
||||
window.form.appendHiddenInputs('#product-create-form, #product-edit-form', 'cross_sells', DataTable.getSelectedIds('#cross_sells .table'));
|
||||
window.form.appendHiddenInputs('#product-create-form, #product-edit-form', 'related_products', DataTable.getSelectedIds('#related_products .table'));
|
||||
|
||||
e.currentTarget.submit();
|
||||
}
|
||||
}
|
||||
5
Modules/Product/Resources/assets/admin/js/main.js
Normal file
5
Modules/Product/Resources/assets/admin/js/main.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import Downloads from './Downloads';
|
||||
import ProductForm from './ProductForm';
|
||||
|
||||
new ProductForm();
|
||||
new Downloads();
|
||||
22
Modules/Product/Resources/assets/admin/sass/main.scss
Normal file
22
Modules/Product/Resources/assets/admin/sass/main.scss
Normal file
@@ -0,0 +1,22 @@
|
||||
#images, #attachments {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.attachment-wrapper {
|
||||
tr {
|
||||
td {
|
||||
&:nth-child(2),
|
||||
&:nth-child(3) {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
width: 59px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.choose-file {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
}
|
||||
25
Modules/Product/Resources/lang/en/attributes.php
Normal file
25
Modules/Product/Resources/lang/en/attributes.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => 'Name',
|
||||
'slug' => 'URL',
|
||||
'description' => 'Description',
|
||||
'short_description' => 'Short Description',
|
||||
'brand_id' => 'Brand',
|
||||
'categories' => 'Categories',
|
||||
'tax_class_id' => 'Tax Class',
|
||||
'tags' => 'Tags',
|
||||
'is_virtual' => 'Virtual',
|
||||
'is_active' => 'Status',
|
||||
'price' => 'Price',
|
||||
'special_price' => 'Special Price',
|
||||
'special_price_type' => 'Special Price Type',
|
||||
'special_price_start' => 'Special Price Start',
|
||||
'special_price_end' => 'Special Price End',
|
||||
'sku' => 'SKU',
|
||||
'manage_stock' => 'Inventory Management',
|
||||
'qty' => 'Qty',
|
||||
'in_stock' => 'Stock Availability',
|
||||
'new_from' => 'Product New From',
|
||||
'new_to' => 'Product New To',
|
||||
];
|
||||
8
Modules/Product/Resources/lang/en/permissions.php
Normal file
8
Modules/Product/Resources/lang/en/permissions.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'index' => 'Index Products',
|
||||
'create' => 'Create Products',
|
||||
'edit' => 'Edit Products',
|
||||
'destroy' => 'Delete Products',
|
||||
];
|
||||
50
Modules/Product/Resources/lang/en/products.php
Normal file
50
Modules/Product/Resources/lang/en/products.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'product' => 'Product',
|
||||
'products' => 'Products',
|
||||
'table' => [
|
||||
'thumbnail' => 'Thumbnail',
|
||||
'name' => 'Name',
|
||||
'price' => 'Price',
|
||||
],
|
||||
'tabs' => [
|
||||
'group' => [
|
||||
'basic_information' => 'Basic Information',
|
||||
'advanced_information' => 'Advanced Information',
|
||||
],
|
||||
'general' => 'General',
|
||||
'price' => 'Price',
|
||||
'inventory' => 'Inventory',
|
||||
'images' => 'Images',
|
||||
'downloads' => 'Downloads',
|
||||
'seo' => 'SEO',
|
||||
'related_products' => 'Related Products',
|
||||
'up_sells' => 'Up-Sells',
|
||||
'cross_sells' => 'Cross-Sells',
|
||||
'additional' => 'Additional',
|
||||
],
|
||||
'form' => [
|
||||
'the_product_won\'t_be_shipped' => 'The product won\'t be shipped',
|
||||
'enable_the_product' => 'Enable the product',
|
||||
'price_types' => [
|
||||
'fixed' => 'Fixed',
|
||||
'percent' => 'Percent',
|
||||
],
|
||||
'manage_stock_states' => [
|
||||
'0' => 'Don\'t Track Inventory',
|
||||
'1' => 'Track Inventory',
|
||||
],
|
||||
'stock_availability_states' => [
|
||||
'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',
|
||||
],
|
||||
];
|
||||
6
Modules/Product/Resources/lang/en/sidebar.php
Normal file
6
Modules/Product/Resources/lang/en/sidebar.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'products' => 'Products',
|
||||
'catalog' => 'Catalog',
|
||||
];
|
||||
@@ -0,0 +1,18 @@
|
||||
@extends('admin::layout')
|
||||
|
||||
@component('admin::components.page.header')
|
||||
@slot('title', trans('admin::resource.create', ['resource' => trans('product::products.product')]))
|
||||
|
||||
<li><a href="{{ route('admin.products.index') }}">{{ trans('product::products.products') }}</a></li>
|
||||
<li class="active">{{ trans('admin::resource.create', ['resource' => trans('product::products.product')]) }}</li>
|
||||
@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() }}
|
||||
|
||||
{!! $tabs->render(compact('product')) !!}
|
||||
</form>
|
||||
@endsection
|
||||
|
||||
@include('product::admin.products.partials.shortcuts')
|
||||
@@ -0,0 +1,20 @@
|
||||
@extends('admin::layout')
|
||||
|
||||
@component('admin::components.page.header')
|
||||
@slot('title', trans('admin::resource.edit', ['resource' => trans('product::products.product')]))
|
||||
@slot('subtitle', $product->name)
|
||||
|
||||
<li><a href="{{ route('admin.products.index') }}">{{ trans('product::products.products') }}</a></li>
|
||||
<li class="active">{{ trans('admin::resource.edit', ['resource' => trans('product::products.product')]) }}</li>
|
||||
@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') }}
|
||||
|
||||
{!! $tabs->render(compact('product')) !!}
|
||||
</form>
|
||||
@endsection
|
||||
|
||||
@include('product::admin.products.partials.shortcuts')
|
||||
@@ -0,0 +1,33 @@
|
||||
@extends('admin::layout')
|
||||
|
||||
@component('admin::components.page.header')
|
||||
@slot('title', trans('product::products.products'))
|
||||
|
||||
<li class="active">{{ trans('product::products.products') }}</li>
|
||||
@endcomponent
|
||||
|
||||
@component('admin::components.page.index_table')
|
||||
@slot('buttons', ['create'])
|
||||
@slot('resource', 'products')
|
||||
@slot('name', trans('product::products.product'))
|
||||
|
||||
@slot('thead')
|
||||
@include('product::admin.products.partials.thead', ['name' => 'products-index'])
|
||||
@endslot
|
||||
@endcomponent
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
new DataTable('#products-table .table', {
|
||||
columns: [
|
||||
{ data: 'checkbox', orderable: false, searchable: false, width: '3%' },
|
||||
{ data: 'id', width: '5%' },
|
||||
{ data: 'thumbnail', orderable: false, searchable: false, width: '10%' },
|
||||
{ data: 'name', name: 'translations.name', orderable: false, defaultContent: '' },
|
||||
{ data: 'price', searchable: false },
|
||||
{ data: 'status', name: 'is_active', searchable: false },
|
||||
{ data: 'created', name: 'created_at' },
|
||||
],
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,14 @@
|
||||
@push('shortcuts')
|
||||
<dl class="dl-horizontal">
|
||||
<dt><code>b</code></dt>
|
||||
<dd>{{ trans('admin::admin.shortcuts.back_to_index', ['name' => trans('product::products.product')]) }}</dd>
|
||||
</dl>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
keypressAction([
|
||||
{ key: 'b', route: "{{ route('admin.products.index') }}" },
|
||||
]);
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,10 @@
|
||||
<tr>
|
||||
@include('admin::partials.table.select_all')
|
||||
|
||||
<th>{{ trans('admin::admin.table.id') }}</th>
|
||||
<th>{{ trans('product::products.table.thumbnail') }}</th>
|
||||
<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>
|
||||
</tr>
|
||||
@@ -0,0 +1,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
{{ Form::textarea('short_description', trans('product::attributes.short_description'), $errors, $product) }}
|
||||
{{ Form::text('new_from', trans('product::attributes.new_from'), $errors, $product, ['class' => 'datetime-picker']) }}
|
||||
{{ Form::text('new_to', trans('product::attributes.new_to'), $errors, $product, ['class' => 'datetime-picker'] ) }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,111 @@
|
||||
<style>
|
||||
.slide {
|
||||
border: 1px solid #e9e9e9;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.slide .slide-header {
|
||||
padding: 15px;
|
||||
background: #f6f6f7;
|
||||
border-bottom: 1px solid #e9e9e9;
|
||||
}
|
||||
|
||||
.slide .slide-header span {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.slide .slide-body {
|
||||
position: relative;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.product-downloads-wrapper .slide {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.product-downloads-wrapper .table > tbody > tr > td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.product-downloads-wrapper .options .drag-icon {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.product-downloads-wrapper .choose-file-group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.product-downloads-wrapper .download-name {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.product-downloads-wrapper .btn-choose-file {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.product-downloads-wrapper .table > tbody > tr {
|
||||
border-top: 1px solid #e9e9e9;
|
||||
}
|
||||
|
||||
.product-downloads-wrapper .table > tbody > tr > td:nth-child(2),
|
||||
.product-downloads-wrapper .table > tbody > tr > td:nth-child(3) {
|
||||
display: block;
|
||||
border: none;
|
||||
width: auto;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
text-align: left;
|
||||
vertical-align: initial;
|
||||
}
|
||||
|
||||
.product-downloads-wrapper .table > tbody > tr > td:nth-child(3) {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.product-downloads-wrapper .options .drag-icon {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="product-downloads-wrapper" class="product-downloads-wrapper clearfix">
|
||||
<div class="slide">
|
||||
<div class="slide-header clearfix">
|
||||
<span class="pull-left">
|
||||
{{ trans('product::products.form.downloadable_files') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="slide-body">
|
||||
<div class="table-responsive">
|
||||
<table class="options table table-bordered">
|
||||
<thead class="hidden-xs">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{ trans('product::products.form.file') }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody id="downloads-wrapper">
|
||||
{{-- Downloadable file will be added here dynamically using JS --}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-default" id="add-new-file">
|
||||
{{ trans('product::products.form.add_new_file') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@include('product::admin.products.tabs.templates.download')
|
||||
|
||||
@push('globals')
|
||||
<script>
|
||||
FleetCart.data['product.downloads'] = {!! old_json('downloads', $product->downloads ?? []) !!};
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,13 @@
|
||||
{{ Form::text('name', trans('product::attributes.name'), $errors, $product, ['labelCol' => 2, 'required' => true]) }}
|
||||
{{ Form::wysiwyg('description', trans('product::attributes.description'), $errors, $product, ['labelCol' => 2, 'required' => true]) }}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
{{ Form::select('brand_id', trans('product::attributes.brand_id'), $errors, $brands, $product) }}
|
||||
{{ Form::select('categories', trans('product::attributes.categories'), $errors, $categories, $product, ['class' => 'selectize prevent-creation', 'multiple' => true]) }}
|
||||
{{ Form::select('tax_class_id', trans('product::attributes.tax_class_id'), $errors, $taxClasses, $product) }}
|
||||
{{ Form::select('tags', trans('product::attributes.tags'), $errors, $tags, $product, ['class' => 'selectize prevent-creation', 'multiple' => true]) }}
|
||||
{{ Form::checkbox('is_virtual', trans('product::attributes.is_virtual'), trans('product::products.form.the_product_won\'t_be_shipped'), $errors, $product) }}
|
||||
{{ Form::checkbox('is_active', trans('product::attributes.is_active'), trans('product::products.form.enable_the_product'), $errors, $product, ['checked' => true]) }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
@include('media::admin.image_picker.single', [
|
||||
'title' => trans('product::products.form.base_image'),
|
||||
'inputName' => 'files[base_image]',
|
||||
'file' => $product->base_image,
|
||||
])
|
||||
|
||||
<div class="media-picker-divider"></div>
|
||||
|
||||
@include('media::admin.image_picker.multiple', [
|
||||
'title' => trans('product::products.form.additional_images'),
|
||||
'inputName' => 'files[additional_images][]',
|
||||
'files' => $product->additional_images,
|
||||
])
|
||||
@@ -0,0 +1,12 @@
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
{{ Form::text('sku', trans('product::attributes.sku'), $errors, $product) }}
|
||||
{{ Form::select('manage_stock', trans('product::attributes.manage_stock'), $errors, trans('product::products.form.manage_stock_states'), $product) }}
|
||||
|
||||
<div class="{{ old('manage_stock', $product->manage_stock) ? '' : 'hide' }}" id="qty-field">
|
||||
{{ Form::number('qty', trans('product::attributes.qty'), $errors, $product, ['required' => true]) }}
|
||||
</div>
|
||||
|
||||
{{ Form::select('in_stock', trans('product::attributes.in_stock'), $errors, trans('product::products.form.stock_availability_states'), $product) }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
{{ Form::number('price', trans('product::attributes.price'), $errors, $product, ['min' => 0, 'required' => true]) }}
|
||||
{{ Form::number('special_price', trans('product::attributes.special_price'), $errors, $product, ['min' => 0]) }}
|
||||
{{ Form::select('special_price_type', trans('product::attributes.special_price_type'), $errors, trans('product::products.form.price_types'), $product) }}
|
||||
{{ Form::text('special_price_start', trans('product::attributes.special_price_start'), $errors, $product, ['class' => 'datetime-picker']) }}
|
||||
{{ Form::text('special_price_end', trans('product::attributes.special_price_end'), $errors, $product, ['class' => 'datetime-picker']) }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,36 @@
|
||||
@component('admin::components.table')
|
||||
@slot('thead')
|
||||
@include('product::admin.products.partials.thead')
|
||||
@endslot
|
||||
@endcomponent
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
@if ($name === 'related_products')
|
||||
DataTable.setSelectedIds('#related_products .table', {!! old_json('related_products', $product->relatedProductList(), JSON_NUMERIC_CHECK) !!});
|
||||
@elseif ($name === 'up_sells')
|
||||
DataTable.setSelectedIds('#up_sells .table', {!! old_json('up_sells', $product->upSellProductList(), JSON_NUMERIC_CHECK) !!});
|
||||
@elseif ($name === 'cross_sells')
|
||||
DataTable.setSelectedIds('#cross_sells .table', {!! old_json('cross_sells', $product->crossSellProductList(), JSON_NUMERIC_CHECK) !!});
|
||||
@endif
|
||||
|
||||
DataTable.setRoutes('#{{ $name }} .table', {
|
||||
index: { name: 'admin.products.index', params: { except: {!! $product->id ?? "''" !!} } },
|
||||
edit: 'admin.products.edit',
|
||||
destroy: 'admin.products.destroy',
|
||||
});
|
||||
|
||||
new DataTable('#{{ $name }} .table', {
|
||||
pageLength: 10,
|
||||
columns: [
|
||||
{ data: 'checkbox', orderable: false, searchable: false, width: '3%' },
|
||||
{ data: 'id', width: '5%' },
|
||||
{ data: 'thumbnail', orderable: false, searchable: false, width: '10%' },
|
||||
{ data: 'name', name: 'translations.name', orderable: false, defaultContent: '' },
|
||||
{ data: 'price', searchable: false },
|
||||
{ data: 'status', name: 'is_active', searchable: false },
|
||||
{ data: 'created', name: 'created_at' },
|
||||
],
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
@include('meta::admin.meta_fields', ['entity' => $product])
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script type="text/html" id="product-download-template">
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span class="drag-icon">
|
||||
<i class="fa"></i>
|
||||
<i class="fa"></i>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div class="form-group">
|
||||
<label class="visible-xs">
|
||||
{{ trans('product::products.form.file') }}
|
||||
</label>
|
||||
|
||||
<div class="choose-file-group">
|
||||
<input
|
||||
type="text"
|
||||
value="<%- download.filename %>"
|
||||
class="form-control download-name"
|
||||
readonly
|
||||
>
|
||||
|
||||
<span class="btn btn-default btn-choose-file">
|
||||
{{ trans('product::products.form.choose') }}
|
||||
</span>
|
||||
|
||||
<input
|
||||
type="hidden"
|
||||
name="files[downloads][]"
|
||||
value="<%- download.id %>"
|
||||
class="download-file"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="text-center">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default delete-row"
|
||||
data-toggle="tooltip"
|
||||
data-title="{{ trans('product::products.form.delete_file') }}"
|
||||
>
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</script>
|
||||
39
Modules/Product/Routes/admin.php
Normal file
39
Modules/Product/Routes/admin.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('products', [
|
||||
'as' => 'admin.products.index',
|
||||
'uses' => 'ProductController@index',
|
||||
'middleware' => 'can:admin.products.index',
|
||||
]);
|
||||
|
||||
Route::get('products/create', [
|
||||
'as' => 'admin.products.create',
|
||||
'uses' => 'ProductController@create',
|
||||
'middleware' => 'can:admin.products.create',
|
||||
]);
|
||||
|
||||
Route::post('products', [
|
||||
'as' => 'admin.products.store',
|
||||
'uses' => 'ProductController@store',
|
||||
'middleware' => 'can:admin.products.create',
|
||||
]);
|
||||
|
||||
Route::get('products/{id}/edit', [
|
||||
'as' => 'admin.products.edit',
|
||||
'uses' => 'ProductController@edit',
|
||||
'middleware' => 'can:admin.products.edit',
|
||||
]);
|
||||
|
||||
Route::put('products/{id}', [
|
||||
'as' => 'admin.products.update',
|
||||
'uses' => 'ProductController@update',
|
||||
'middleware' => 'can:admin.products.edit',
|
||||
]);
|
||||
|
||||
Route::delete('products/{ids}', [
|
||||
'as' => 'admin.products.destroy',
|
||||
'uses' => 'ProductController@destroy',
|
||||
'middleware' => 'can:admin.products.destroy',
|
||||
]);
|
||||
10
Modules/Product/Routes/public.php
Normal file
10
Modules/Product/Routes/public.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
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');
|
||||
52
Modules/Product/Services/ChosenProductOptions.php
Normal file
52
Modules/Product/Services/ChosenProductOptions.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Services;
|
||||
|
||||
use Modules\Option\Entities\Option;
|
||||
use Modules\Product\Entities\Product;
|
||||
|
||||
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()
|
||||
->with(['values' => function ($query) {
|
||||
$query->whereIn('id', array_flatten($this->chosenOptions));
|
||||
}])
|
||||
->whereIn('id', array_keys($this->chosenOptions))
|
||||
->get()
|
||||
->filter(function ($productOption) {
|
||||
return $productOption->values->isNotEmpty();
|
||||
});
|
||||
|
||||
return $this->mergeTextTypeOptions($productOptions);
|
||||
}
|
||||
|
||||
private function mergeTextTypeOptions($productOptions)
|
||||
{
|
||||
$filteredOptions = collect($this->chosenOptions)->reject(function ($_, $optionId) use ($productOptions) {
|
||||
return $productOptions->contains('id', $optionId);
|
||||
});
|
||||
|
||||
$textTypeOptions = Option::with('values')
|
||||
->whereIn('id', $filteredOptions->keys())
|
||||
->get();
|
||||
|
||||
return $filteredOptions->map(function ($value, $optionId) use ($textTypeOptions) {
|
||||
return optional($textTypeOptions->where('id', $optionId)->first(), function ($option) use ($value) {
|
||||
$option->values->first()->fill(['label' => $value]);
|
||||
|
||||
return $option;
|
||||
});
|
||||
})->merge($productOptions);
|
||||
}
|
||||
}
|
||||
39
Modules/Product/Sidebar/SidebarExtender.php
Normal file
39
Modules/Product/Sidebar/SidebarExtender.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Product\Sidebar;
|
||||
|
||||
use Maatwebsite\Sidebar\Item;
|
||||
use Maatwebsite\Sidebar\Menu;
|
||||
use Maatwebsite\Sidebar\Group;
|
||||
use Modules\Admin\Sidebar\BaseSidebarExtender;
|
||||
|
||||
class SidebarExtender extends BaseSidebarExtender
|
||||
{
|
||||
public function extend(Menu $menu)
|
||||
{
|
||||
$menu->group(trans('admin::sidebar.content'), function (Group $group) {
|
||||
$group->item(trans('product::sidebar.products'), function (Item $item) {
|
||||
$item->icon('fa fa-cube');
|
||||
$item->weight(10);
|
||||
$item->route('admin.products.index');
|
||||
$item->authorize(
|
||||
$this->auth->hasAnyAccess([
|
||||
'admin.products.index',
|
||||
'admin.categories.index',
|
||||
'admin.attributes.index',
|
||||
'admin.attribute_sets.index',
|
||||
'admin.options.index',
|
||||
])
|
||||
);
|
||||
|
||||
$item->item(trans('product::sidebar.catalog'), function (Item $item) {
|
||||
$item->weight(5);
|
||||
$item->route('admin.products.index');
|
||||
$item->authorize(
|
||||
$this->auth->hasAccess('admin.products.index')
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
28
Modules/Product/composer.json
Normal file
28
Modules/Product/composer.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "fleetcart/product",
|
||||
"description": "The FleetCart Product Module.",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Envay Soft",
|
||||
"email": "envaysoft@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.0.2",
|
||||
"darryldecode/cart": "^4.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Modules\\Product\\": ""
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
},
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
39
Modules/Product/helpers.php
Normal file
39
Modules/Product/helpers.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Modules\FlashSale\Entities\FlashSale;
|
||||
|
||||
if (! function_exists('product_price_formatted')) {
|
||||
/**
|
||||
* Get the selling price of the given product.
|
||||
*
|
||||
* @param \Modules\Product\Entities\Product $product
|
||||
* @param \Closure $callback
|
||||
* @return string
|
||||
*/
|
||||
function product_price_formatted($product, $callback = null)
|
||||
{
|
||||
if (FlashSale::contains($product)) {
|
||||
$previousPrice = $product->getSellingPrice()->convertToCurrentCurrency()->format();
|
||||
$flashSalePrice = FlashSale::pivot($product)->price->convertToCurrentCurrency()->format();
|
||||
|
||||
if (is_callable($callback)) {
|
||||
return $callback($flashSalePrice, $previousPrice);
|
||||
}
|
||||
|
||||
return "{$flashSalePrice} <span class='previous-price'>{$previousPrice}</span>";
|
||||
}
|
||||
|
||||
$price = $product->price->convertToCurrentCurrency()->format();
|
||||
$specialPrice = $product->getSpecialPrice()->convertToCurrentCurrency()->format();
|
||||
|
||||
if (is_callable($callback)) {
|
||||
return $callback($price, $specialPrice);
|
||||
}
|
||||
|
||||
if (! $product->hasSpecialPrice()) {
|
||||
return $price;
|
||||
}
|
||||
|
||||
return "{$specialPrice} <span class='previous-price'>{$price}</span>";
|
||||
}
|
||||
}
|
||||
13
Modules/Product/module.json
Normal file
13
Modules/Product/module.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "Product",
|
||||
"alias": "product",
|
||||
"description": "The product module is responsible for managing products.",
|
||||
"priority": 100,
|
||||
"providers": [
|
||||
"Modules\\Product\\Providers\\ProductServiceProvider",
|
||||
"Modules\\Product\\Providers\\EventServiceProvider"
|
||||
],
|
||||
"files": [
|
||||
"helpers.php"
|
||||
]
|
||||
}
|
||||
8
Modules/Product/webpack.mix.js
Normal file
8
Modules/Product/webpack.mix.js
Normal file
@@ -0,0 +1,8 @@
|
||||
let mix = require('laravel-mix');
|
||||
let execSync = require('child_process').execSync;
|
||||
|
||||
mix.js(`${__dirname}/Resources/assets/admin/js/main.js`, `${__dirname}/Assets/admin/js/product.js`)
|
||||
.sass(`${__dirname}/Resources/assets/admin/sass/main.scss`, `${__dirname}/Assets/admin/css/product.css`)
|
||||
.then(() => {
|
||||
execSync(`npm run rtlcss ${__dirname}/Assets/admin/css/product.css ${__dirname}/Assets/admin/css/product.rtl.css`);
|
||||
});
|
||||
Reference in New Issue
Block a user