Bundle add to cart added

This commit is contained in:
jitendra 2019-08-30 17:09:15 +05:30
parent 7352a97797
commit 557cbb1db1
32 changed files with 961 additions and 185 deletions

View File

@ -221,11 +221,6 @@
inject: ['$validator'],
data: function() {
return {
}
},
computed: {
titleInputName: function () {
if (this.option.id)

View File

@ -2,9 +2,10 @@
namespace Webkul\Category\Http\Controllers;
use Illuminate\Support\Facades\Event;
use Webkul\Category\Repositories\CategoryRepository;
use Webkul\Category\Models\CategoryTranslation;
use Illuminate\Support\Facades\Event;
use Webkul\Attribute\Repositories\AttributeRepository;
/**
* Catalog category controller

View File

@ -657,10 +657,7 @@ class Cart {
return false;
} else {
foreach ($cart->items as $item) {
if ($item->product_flat->getTypeInstance()->getMinimalPrice($item) == $item->price)
continue;
$price = ! is_null($item->custom_price) ? $item->custom_price : $productFlat->getTypeInstance()->getMinimalPrice();
$price = ! is_null($item->custom_price) ? $item->custom_price : $item->base_price;
$this->cartItemRepository->update([
'price' => core()->convertPrice($price),
@ -871,8 +868,10 @@ class Cart {
'additional' => $data['additional'],
];
if (isset($data['child']) && $data['child']) {
$finalData['child'] = $this->prepareDataForOrderItem($data['child']);
if (isset($data['children']) && $data['children']) {
foreach ($data['children'] as $child) {
$finalData['children'][] = $this->prepareDataForOrderItem($child);
}
}
return $finalData;

View File

@ -14,7 +14,7 @@ class Cart extends Model implements CartContract
protected $hidden = ['coupon_code'];
protected $with = ['items', 'items.child'];
protected $with = ['items', 'items.children'];
/**
* To get relevant associated items with the cart instance

View File

@ -436,6 +436,28 @@ class Core
}
}
/**
* Format and convert price with currency symbol
*
* @return array
*/
public function getAccountJsSymbols()
{
$formater = new \NumberFormatter( app()->getLocale(), \NumberFormatter::CURRENCY );
$pattern = $formater->getPattern();
$pattern = str_replace("¤", "%s", $pattern);
$pattern = str_replace("#,##0.00", "%v", $pattern);
return [
'symbol' => core()->currencySymbol(core()->getCurrentCurrencyCode()),
'decimal' => $formater->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL),
'format' => $pattern
];
}
/**
* Format price with base currency symbol
*

View File

@ -16,6 +16,7 @@ class CreateProductBundleOptionProductsTable extends Migration
Schema::create('product_bundle_option_products', function (Blueprint $table) {
$table->increments('id');
$table->integer('qty')->default(0);
$table->boolean('is_user_defined')->default(1);
$table->boolean('is_default')->default(0);
$table->integer('sort_order')->default(0);

View File

@ -0,0 +1,111 @@
<?php
namespace Webkul\Product\Helpers;
/**
* Bundle Option Helper
*
* @author Jitendra Singh <jitendra@webkul.com>
* @copyright 2018 Webkul Software Pvt Ltd (http://www.webkul.com)
*/
class BundleOption extends AbstractProduct
{
/**
* Product
*
* @var Product
*/
protected $product;
/**
* Returns bundle option config
*
* @param Product $product
* @return array
*/
public function getBundleConfig($product)
{
$this->product = $product;
return [
'options' => $this->getOptions()
];
}
/**
* Returns bundle options
*
* @return array
*/
public function getOptions()
{
$options = [];
foreach ($this->product->bundle_options as $option) {
$options[$option->id] = $this->getOptionItemData($option);
}
return $options;
}
/**
* Get formed data from bundle option
*
* @param ProductBundleOption $option
* @return array
*/
private function getOptionItemData($option)
{
return [
'id' => $option->id,
'label' => $option->label,
'type' => $option->type,
'products' => $this->getOptionProducts($option),
'sort_order' => $option->sort_order
];
}
/**
* Get formed data from bundle option product
*
* @param ProductBundleOption $option
* @return array
*/
private function getOptionProducts($option)
{
$products = [];
foreach ($option->bundle_option_products as $index => $bundleOptionProduct) {
$products[$bundleOptionProduct->id] = [
'id' => $bundleOptionProduct->id,
'qty' => $bundleOptionProduct->qty,
'price' => $this->getProductPrices($bundleOptionProduct->product),
'name' => $bundleOptionProduct->product->name,
'product_id' => $bundleOptionProduct->product_id,
'is_default' => $bundleOptionProduct->is_default
];
}
return $products;
}
/**
* Returns bundle product prices
*
* @param Product $product
* @return array
*/
public function getProductPrices($product)
{
return [
'regular_price' => [
'price' => core()->convertPrice($product->price),
'formated_price' => core()->currency($product->price)
],
'final_price' => [
'price' => core()->convertPrice($product->getTypeInstance()->getMinimalPrice()),
'formated_price' => core()->currency($product->getTypeInstance()->getMinimalPrice())
]
];
}
}

View File

@ -71,7 +71,7 @@ class ConfigurableOption extends AbstractProduct
* Returns the allowed variants JSON
*
* @param Product $product
* @return float
* @return array
*/
public function getConfigurationConfig($product)
{

View File

@ -9,7 +9,15 @@ class ProductBundleOptionProduct extends Model implements ProductBundleOptionPro
{
public $timestamps = false;
protected $fillable = ['qty', 'sort_order', 'is_default', 'product_bundle_option_id', 'product_id'];
protected $fillable = ['qty', 'is_user_defined', 'sort_order', 'is_default', 'product_bundle_option_id', 'product_id'];
/**
* Get the bundle option that owns this resource.
*/
public function bundle_option()
{
return $this->belongsTo(ProductBundleProductProxy::modelClass());
}
/**
* Get the product that owns the image.

View File

@ -175,6 +175,14 @@ class ProductFlat extends Model implements ProductFlatContract
return $this->product->grouped_products();
}
/**
* Get the bundle options that owns the product.
*/
public function bundle_options()
{
return $this->product->bundle_options();
}
/**
* Retrieve product attributes
*

View File

@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\ModelNotFoundException;
use Webkul\Attribute\Repositories\AttributeRepository;
use Webkul\Core\Eloquent\Repository;
use Webkul\Product\Repositories\ProductFlatRepository;
use Webkul\Product\Models\ProductAttributeValue;
/**
* Product Repository
@ -171,7 +172,7 @@ class ProductRepository extends Repository
foreach($aliases as $table => $alias) {
$query1 = $query1->orWhere(function($query2) use($qb, $table, $alias) {
foreach ($this->attribute->getProductDefaultAttributes(array_keys(request()->input())) as $code => $attribute) {
foreach ($this->attributeRepository->getProductDefaultAttributes(array_keys(request()->input())) as $code => $attribute) {
$aliasTemp = $alias . $attribute->code;
$qb = $qb->leftJoin('product_attribute_values as ' . $aliasTemp, $table . '.id', '=', $aliasTemp . '.product_id');

View File

@ -69,6 +69,41 @@ abstract class AbstractType
*/
protected $product;
/**
* Is a composite product type
*
* @var boolean
*/
protected $isComposite = false;
/**
* Is a stokable product type
*
* @var boolean
*/
protected $isStockable = true;
/**
* Show quantity box
*
* @var boolean
*/
protected $showQuantityBox = false;
/**
* Is product have sufficient quantity
*
* @var boolean
*/
protected $haveSufficientQuantity = true;
/**
* Product can be moved from wishlist to cart or not
*
* @var boolean
*/
protected $canBeMovedFromWishlistToCart = true;
/**
* Create a new product type instance.
*
@ -213,7 +248,17 @@ abstract class AbstractType
*/
public function isStockable()
{
return true;
return $this->isStockable;
}
/**
* Return true if this product can have inventory
*
* @return boolean
*/
public function isComposite()
{
return $this->isComposite;
}
/**
@ -222,7 +267,7 @@ abstract class AbstractType
*/
public function haveSufficientQuantity($qty)
{
return true;
return $this->haveSufficientQuantity;
}
/**
@ -232,7 +277,7 @@ abstract class AbstractType
*/
public function showQuantityBox()
{
return false;
return $this->showQuantityBox;
}
/**
@ -251,7 +296,7 @@ abstract class AbstractType
*/
public function canBeMovedFromWishlistToCart($item)
{
return true;
return $this->canBeMovedFromWishlistToCart;
}
/**
@ -315,6 +360,16 @@ abstract class AbstractType
return $this->product->price;
}
/**
* Get product minimal price
*
* @return float
*/
public function getFinalPrice()
{
return $this->getMinimalPrice();
}
/**
* Returns the product's minimal price
*
@ -370,7 +425,7 @@ abstract class AbstractType
if ($this->isStockable() && ! $this->haveSufficientQuantity($data['quantity']))
return trans('shop::app.checkout.cart.quantity.inventory_warning');
$price = $this->getMinimalPrice();
$price = $this->getFinalPrice();
$products = [
[
@ -408,16 +463,6 @@ abstract class AbstractType
return $data;
}
/**
* Check if product can be configured
*
* @return boolean
*/
public function canConfigure()
{
return false;
}
/**
*
* @param array $options1

View File

@ -8,6 +8,7 @@ use Webkul\Product\Repositories\ProductAttributeValueRepository;
use Webkul\Product\Repositories\ProductInventoryRepository;
use Webkul\Product\Repositories\ProductImageRepository;
use Webkul\Product\Repositories\ProductBundleOptionRepository;
use Webkul\Product\Repositories\ProductBundleOptionProductRepository;
use Webkul\Product\Helpers\ProductImage;
/**
@ -60,6 +61,13 @@ class Bundle extends AbstractType
*/
protected $productBundleOptionRepository;
/**
* ProductBundleOptionProductRepository instance
*
* @var ProductBundleOptionProductRepository
*/
protected $productBundleOptionProductRepository;
/**
* Product Image helper instance
*
@ -86,16 +94,24 @@ class Bundle extends AbstractType
'admin::catalog.products.accordians.product-links'
];
/**
* Is a composite product type
*
* @var boolean
*/
protected $isComposite = true;
/**
* Create a new product type instance.
*
* @param Webkul\Attribute\Repositories\AttributeRepository $attributeRepository
* @param Webkul\Product\Repositories\ProductRepository $productRepository
* @param Webkul\Product\Repositories\ProductAttributeValueRepository $attributeValueRepository
* @param Webkul\Product\Repositories\ProductInventoryRepository $productInventoryRepository
* @param Webkul\Product\Repositories\ProductImageRepository $productImageRepository
* @param Webkul\Product\Repositories\ProductBundleOptionRepository $productBundleOptionRepository
* @param Webkul\Product\Helpers\ProductImage $productImageHelper
* @param Webkul\Attribute\Repositories\AttributeRepository $attributeRepository
* @param Webkul\Product\Repositories\ProductRepository $productRepository
* @param Webkul\Product\Repositories\ProductAttributeValueRepository $attributeValueRepository
* @param Webkul\Product\Repositories\ProductInventoryRepository $productInventoryRepository
* @param Webkul\Product\Repositories\ProductImageRepository $productImageRepository
* @param Webkul\Product\Repositories\ProductBundleOptionRepository $productBundleOptionRepository
* @param Webkul\Product\Repositories\ProductBundleOptionProductRepository $productBundleOptionProductRepository
* @param Webkul\Product\Helpers\ProductImage $productImageHelper
* @return void
*/
public function __construct(
@ -105,6 +121,7 @@ class Bundle extends AbstractType
ProductInventoryRepository $productInventoryRepository,
productImageRepository $productImageRepository,
ProductBundleOptionRepository $productBundleOptionRepository,
ProductBundleOptionProductRepository $productBundleOptionProductRepository,
ProductImage $productImageHelper
)
{
@ -118,6 +135,8 @@ class Bundle extends AbstractType
);
$this->productBundleOptionRepository = $productBundleOptionRepository;
$this->productBundleOptionProductRepository = $productBundleOptionProductRepository;
}
/**
@ -135,4 +154,158 @@ class Bundle extends AbstractType
return $product;
}
/**
* Get product minimal price
*
* @return float
*/
public function getMinimalPrice()
{
return 0;
}
/**
* Get product maximam price
*
* @return float
*/
public function getMaximamPrice()
{
return 0;
}
/**
* Get product final price
*
* @return float
*/
public function getFinalPrice()
{
return 0;
}
/**
* Get product minimal price
*
* @return string
*/
public function getPriceHtml()
{
}
/**
* Add product. Returns error message if can't prepare product.
*
* @param array $data
* @return array
*/
public function prepareForCart($data)
{
if (! isset($data['bundle_options']))
return trans('shop::app.checkout.cart.item.error-add');
$products = parent::prepareForCart($data);
foreach ($this->getCartChildProducts($data) as $productId => $data) {
$product = $this->productRepository->find($productId);
$cartProduct = $product->getTypeInstance()->prepareForCart($data);
if (is_string($cartProduct))
return $cartProduct;
$cartProduct[0]['parent_id'] = $this->product->id;
$products = array_merge($products, $cartProduct);
$products[0]['price'] += $cartProduct[0]['total'];
$products[0]['base_price'] += $cartProduct[0]['base_total'];
$products[0]['total'] += $cartProduct[0]['total'];
$products[0]['base_total'] += $cartProduct[0]['base_total'];
$products[0]['weight'] += $cartProduct[0]['weight'];
$products[0]['total_weight'] += $cartProduct[0]['total_weight'];
$products[0]['base_total_weight'] += $cartProduct[0]['base_total_weight'];
}
return $products;
}
/**
* Add product. Returns error message if can't prepare product.
*
* @param array $data
* @return array
*/
public function getCartChildProducts($data)
{
$products = [];
foreach ($data['bundle_options'] as $optionId => $optionProductIds) {
foreach ($optionProductIds as $optionProductId) {
$optionProduct = $this->productBundleOptionProductRepository->findOneWhere([
'id' => $optionProductId,
'product_bundle_option_id' => $optionId
]);
$qty = $data['bundle_option_qty'][$optionId] ?? $optionProduct->qty;
if (! isset($products[$optionProduct->product_id])) {
$products[$optionProduct->product_id] = [
'product_id' => $optionProduct->product_id,
'quantity' => $qty,
];
} else {
$products[$optionProduct->product_id] = array_merge($products[$optionProduct->product_id], [
'quantity' => $products[$optionProduct->product_id]['quantity'] + $qty
]);
}
}
}
return $products;
}
/**
*
* @param array $options1
* @param array $options2
* @return boolean
*/
public function compareOptions($options1, $options2)
{
if ($this->product->id != $options2['product_id'])
return false;
return $options1['bundle_options'] == $options2['bundle_options'];
}
/**
* Returns additional information for items
*
* @param array $data
* @return array
*/
public function getAdditionalOptions($data)
{
foreach ($data['bundle_options'] as $optionId => $optionProductIds) {
$option = $this->productBundleOptionRepository->find($optionId);
$labels = [];
foreach ($optionProductIds as $optionProductId) {
$optionProduct = $this->productBundleOptionProductRepository->find($optionProductId);
$labels[] = $optionProduct->product->name;
}
$data['attributes'][$option->id] = [
'attribute_name' => $option->label,
'option_id' => $option->id,
'option_label' => implode(', ', $labels),
];
}
return $data;
}
}

View File

@ -32,6 +32,20 @@ class Configurable extends AbstractType
'admin::catalog.products.accordians.product-links'
];
/**
* Is a composite product type
*
* @var boolean
*/
protected $isComposite = true;
/**
* Show quantity box
*
* @var boolean
*/
protected $showQuantityBox = true;
/**
* @param array $data
* @return Product
@ -264,16 +278,6 @@ class Configurable extends AbstractType
{
return $cartItem->child->product->getTypeInstance()->haveSufficientQuantity($cartItem->quantity);
}
/**
* Return true if this product can have inventory
*
* @return boolean
*/
public function showQuantityBox()
{
return true;
}
/**
* Returns validation rules
@ -380,7 +384,7 @@ class Configurable extends AbstractType
if (! $childProduct->haveSufficientQuantity($data['quantity']))
return trans('shop::app.checkout.cart.quantity.inventory_warning');
$price = $this->getMinimalPrice();
$price = $this->getFinalPrice();
$products = [
[
@ -410,16 +414,6 @@ class Configurable extends AbstractType
return $products;
}
/**
* Check if product can be configured
*
* @return boolean
*/
public function canConfigure()
{
return true;
}
/**
*
* @param array $options1

View File

@ -95,6 +95,13 @@ class Downloadable extends AbstractType
'admin::catalog.products.accordians.product-links'
];
/**
* Is a stokable product type
*
* @var boolean
*/
protected $isStockable = false;
/**
* Create a new product type instance.
*
@ -168,16 +175,6 @@ class Downloadable extends AbstractType
return false;
}
/**
* Return true if this product can have inventory
*
* @return boolean
*/
public function isStockable()
{
return false;
}
/**
* Returns validation rules
*
@ -210,16 +207,6 @@ class Downloadable extends AbstractType
return parent::prepareForCart($data);
}
/**
* Check if product can be configured
*
* @return boolean
*/
public function canConfigure()
{
return true;
}
/**
*
* @param array $options1

View File

@ -88,6 +88,13 @@ class Grouped extends AbstractType
'admin::catalog.products.accordians.product-links'
];
/**
* Is a composite product type
*
* @var boolean
*/
protected $isComposite = true;
/**
* Create a new product type instance.
*

View File

@ -29,6 +29,13 @@ class Simple extends AbstractType
'admin::catalog.products.accordians.product-links'
];
/**
* Show quantity box
*
* @var boolean
*/
protected $showQuantityBox = true;
/**
* Return true if this product type is saleable
*
@ -54,16 +61,6 @@ class Simple extends AbstractType
{
return $qty <= $this->totalQuantity() ? true : (core()->getConfigData('catalog.inventory.stock_options.backorders') ? true : false);
}
/**
* Return true if this product can have inventory
*
* @return boolean
*/
public function showQuantityBox()
{
return true;
}
/**
* @return integer

View File

@ -29,12 +29,9 @@ class Virtual extends AbstractType
];
/**
* Return true if this product can have inventory
* Is a stokable product type
*
* @return boolean
* @var boolean
*/
public function isStockable()
{
return false;
}
protected $isStockable = false;
}

View File

@ -8,7 +8,7 @@ use Webkul\Product\Models\Product;
class OrderItem extends Model implements OrderItemContract
{
protected $guarded = ['id', 'child', 'created_at', 'updated_at'];
protected $guarded = ['id', 'child', 'children', 'created_at', 'updated_at'];
protected $casts = [
'additional' => 'array',
@ -129,6 +129,14 @@ class OrderItem extends Model implements OrderItemContract
return $this->hasOne(OrderItemProxy::modelClass(), 'parent_id');
}
/**
* Get the children items.
*/
public function children()
{
return $this->hasMany(self::class, 'parent_id');
}
/**
* Get the invoice items record associated with the order item.
*/

View File

@ -136,10 +136,8 @@ class InvoiceRepository extends Repository
'product_type' => $orderItem->product_type,
'additional' => $orderItem->additional,
]);
if ($orderItem->type == 'configurable' && $orderItem->child) {
$childOrderItem = $orderItem->child;
foreach ($orderItem->children as $childOrderItem) {
$invoiceItem->child = $this->invoiceItemRepository->create([
'invoice_id' => $invoice->id,
'order_item_id' => $childOrderItem->id,

View File

@ -38,9 +38,9 @@ class OrderItemInventoryRepository extends Repository
$product = $orderItem->type == 'configurable' ? $orderItem->child->product : $orderItem->product;
if (!$product) {
if (! $product)
return ;
}
$inventories = $product->inventory_sources()->orderBy('priority', 'asc')->get();
foreach ($inventories as $inventorySource) {

View File

@ -38,7 +38,7 @@ class OrderItemRepository extends Repository
unset($data['product']);
}
return $this->model->create($data);
return parent::create($data);
}
/**
@ -88,26 +88,38 @@ class OrderItemRepository extends Repository
if (! $orderedQuantity = $orderItem->qty_ordered)
return;
$product = $orderItem->type == 'configurable' ? $orderItem->child->product : $orderItem->product;
$products = [];
if (! $product)
return;
$orderedInventory = $product->ordered_inventories()
->where('channel_id', $orderItem->order->channel->id)
->first();
if ($orderedInventory) {
$orderedInventory->update([
'qty' => $orderedInventory->qty + $orderItem->qty_ordered
]);
if ($orderItem->product->getTypeInstance()->isComposite()) {
foreach ($orderItem->children as $child) {
if (! $child->product)
continue;
$products[] = $child->product;
}
} else {
$product->ordered_inventories()->create([
'qty' => $orderItem->qty_ordered,
'product_id' => $product->id,
'channel_id' => $orderItem->order->channel->id,
]);
if (! $orderItem->product)
return;
$products[] = $orderItem->product;
}
foreach ($products as $product) {
$orderedInventory = $product->ordered_inventories()
->where('channel_id', $orderItem->order->channel->id)
->first();
if ($orderedInventory) {
$orderedInventory->update([
'qty' => $orderedInventory->qty + $orderItem->qty_ordered
]);
} else {
$product->ordered_inventories()->create([
'qty' => $orderItem->qty_ordered,
'product_id' => $product->id,
'channel_id' => $orderItem->order->channel->id,
]);
}
}
}

View File

@ -102,8 +102,10 @@ class OrderRepository extends Repository
foreach ($data['items'] as $item) {
$orderItem = $this->orderItemRepository->create(array_merge($item, ['order_id' => $order->id]));
if (isset($item['child']) && $item['child']) {
$orderItem->child = $this->orderItemRepository->create(array_merge($item['child'], ['order_id' => $order->id, 'parent_id' => $orderItem->id]));
if (isset($item['children']) && $item['children']) {
foreach ($item['children'] as $child) {
$this->orderItemRepository->create(array_merge($child, ['order_id' => $order->id, 'parent_id' => $orderItem->id]));
}
}
$this->orderItemRepository->manageInventory($orderItem);

View File

@ -18,6 +18,7 @@
"vue": "^2.1.10"
},
"dependencies": {
"accounting": "^0.4.1",
"ez-plus": "^1.2.1",
"vee-validate": "2.0.0-rc.26",
"vue-flatpickr": "^2.3.0",

View File

@ -4,6 +4,7 @@ window.VeeValidate = require("vee-validate");
window.axios = require("axios");
require("./bootstrap");
require("ez-plus/src/jquery.ez-plus.js");
var accounting = require('accounting');
Vue.use(VeeValidate);
Vue.prototype.$http = axios
@ -12,6 +13,9 @@ window.eventBus = new Vue();
Vue.component("image-slider", require("./components/image-slider.vue"));
Vue.component("vue-slider", require("vue-slider-component"));
Vue.filter('currency', function (value, argument) {
return accounting.formatMoney(value, argument);
})
$(document).ready(function () {

View File

@ -170,6 +170,24 @@ input {
animation: spin 1s ease-in-out infinite;
}
.radio {
margin: 10px 0 0px 0px !important;
}
.checkbox {
margin: 10px 0 0px 0px;
.checkbox-view {
height: 16px !important;
width: 16px !important;
background-image: url("../images/checkbox.svg") !important;
}
input:checked + .checkbox-view {
background-image: url("../images/checkbox-checked.svg") !important;
}
}
.pull-right {
float:right;
}
@ -411,6 +429,7 @@ input {
font-size: 14px;
color: $btn-background-color;
box-shadow: 1px 1px 1px $shadow-color1;
font-weight: 500;
&.sale {
background: $disc-price;
@ -1409,16 +1428,6 @@ section.slider-block {
.checkbox {
margin: 0;
.checkbox-view {
height: 16px;
width: 16px;
background-image: url("../images/checkbox.svg");
}
input:checked + .checkbox-view {
background-image: url("../images/checkbox-checked.svg");
}
}
.color-swatch {
@ -1928,6 +1937,108 @@ section.product-detail {
}
}
.bundle-options-wrapper {
.bundle-option-list {
padding: 15px 0;
border-top: solid 1px rgba(162, 162, 162, 0.2);
h3 {
font-size: 16px;
margin: 0;
color: #242424;
}
.bundle-option-item {
border-bottom: solid 1px rgba(162, 162, 162, 0.2);
padding: 15px 0;
width: 100%;
display: inline-block;
&:last-child {
border-bottom: 0;
padding-bottom: 0;
}
.control-group {
margin-bottom: 0;
color: #5E5E5E;
label {
color: #242424;
}
.control {
color: #5E5E5E;
}
.price {
margin-left: 15px;
}
}
.quantity {
border-top: 0;
padding-bottom: 0;
&.has-error {
button {
border-color: #FC6868;
color: #FC6868;
}
}
}
.control-error {
float: left;
width: 100%;
}
&.has-error {
button {
border-color: #FC6868;
color: #FC6868;
}
}
}
}
.bundle-summary {
padding: 15px 0;
border-top: solid 1px rgba(162, 162, 162, 0.2);
h3 {
font-size: 16px;
margin: 0;
color: #242424;
}
.quantity {
border-top: 0;
}
.bundle-price {
font-weight: 600;
font-size: 24px;
color: #FF6472;
margin-top: 10px;
}
ul.bundle-items {
li {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
.selected-products {
color: #5E5E5E;
}
}
}
}
}
.full-description {
* {
max-width: 100%;
@ -2013,15 +2124,6 @@ section.product-detail {
}
}
}
.quantity-change {
cursor: pointer;
text-align: center;
&:focus {
border-color: $border-color !important;
}
}
}
}
}
@ -2302,14 +2404,6 @@ section.cart {
margin-top: 18px;
margin-right: 15px;
}
.quantity-change {
cursor: pointer;
&:focus {
border-color: $border-color !important;
}
}
}
}
@ -2319,6 +2413,50 @@ section.cart {
}
}
.quantity {
display: inline-block !important;
label {
margin-bottom: 10px;
}
button {
width: 40px;
height: 38px;
font-size: 16px;
background: #ffffff;
border: 1px solid #C7C7C7;
float: left;
cursor: pointer;
&.decrease {
border-radius: 3px 0px 0px 3px;
}
&.increase {
border-radius: 0px 3px 3px 0px;
}
}
&.control-group {
.control {
text-align: center;
float: left;
width: 60px;
height: 38px;
margin: 0;
border: 1px solid #C7C7C7;
border-right: none;
border-left: none;
border-radius: 0px;
&:focus {
border-color: #C7C7C7;
}
}
}
}
.order-summary {
h3 {
font-size: 16px;

View File

@ -324,7 +324,11 @@ return [
'out-of-stock' => 'خارج الأسهم',
'view-all' => 'عرض الكل',
'less-quantity' => 'Quantity can not be less than one.',
'starting-at' => 'Starting at'
'starting-at' => 'Starting at',
'customize-options' => 'Customize Options',
'choose-selection' => 'Choose a selection',
'your-customization' => 'Your Customization',
'total-amount' => 'Total Amount'
],
'wishlist' => [

View File

@ -354,7 +354,11 @@ return [
'sample' => 'Sample',
'name' => 'Name',
'qty' => 'Qty',
'starting-at' => 'Starting at'
'starting-at' => 'Starting at',
'customize-options' => 'Customize Options',
'choose-selection' => 'Choose a selection',
'your-customization' => 'Your Customization',
'total-amount' => 'Total Amount'
],
'wishlist' => [

View File

@ -334,7 +334,11 @@ return [
'view-all' => 'مشاهده همه',
'select-above-options' => 'لطفا ابتدا گزینه های بالا را انتخاب کنید',
'less-quantity' => 'کمیت نمی تواند کمتر از یک باشد.',
'starting-at' => 'Starting at'
'starting-at' => 'Starting at',
'customize-options' => 'Customize Options',
'choose-selection' => 'Choose a selection',
'your-customization' => 'Your Customization',
'total-amount' => 'Total Amount'
],
'wishlist' => [

View File

@ -329,7 +329,11 @@ return [
'view-all' => 'Ver Tudo',
'select-above-options' => 'Por favor, selecione as opções acima primeiro.',
'less-quantity' => 'Quantity can not be less than one.',
'starting-at' => 'Starting at'
'starting-at' => 'Starting at',
'customize-options' => 'Customize Options',
'choose-selection' => 'Choose a selection',
'your-customization' => 'Your Customization',
'total-amount' => 'Total Amount'
],
'wishlist' => [

View File

@ -48,21 +48,11 @@
{!! view_render_event('bagisto.shop.products.view.quantity.before', ['product' => $product]) !!}
@if ($product->getTypeInstance()->showQuantityBox())
<div class="quantity control-group" :class="[errors.has('quantity') ? 'has-error' : '']">
<label class="required">{{ __('shop::app.products.quantity') }}</label>
<input class="control quantity-change" value="-" style="width: 35px; border-radius: 3px 0px 0px 3px;" onclick="updateQunatity('remove')" readonly>
<input name="quantity" id="quantity" class="control quantity-change" value="1" v-validate="'required|numeric|min_value:1'" style="width: 60px; position: relative; margin-left: -4px; margin-right: -4px; border-right: none;border-left: none; border-radius: 0px;" data-vv-as="&quot;{{ __('shop::app.products.quantity') }}&quot;" readonly>
<input class="control quantity-change" value="+" style="width: 35px; padding: 0 12px; border-radius: 0px 3px 3px 0px;" onclick=updateQunatity('add') readonly>
<span class="control-error" v-if="errors.has('quantity')">@{{ errors.first('quantity') }}</span>
</div>
<quantity-changer></quantity-changer>
@else
<input type="hidden" name="quantity" value="1">
@endif
{!! view_render_event('bagisto.shop.products.view.quantity.after', ['product' => $product]) !!}
@ -71,6 +61,8 @@
@include ('shop::products.view.downloadable')
@include ('shop::products.view.grouped-products')
@include ('shop::products.view.bundle-options')
{!! view_render_event('bagisto.shop.products.view.description.before', ['product' => $product]) !!}
@ -118,6 +110,20 @@
</form>
</script>
<script type="text/x-template" id="quantity-changer-template">
<div class="quantity control-group" :class="[errors.has(controlName) ? 'has-error' : '']">
<label class="required">{{ __('shop::app.products.quantity') }}</label>
<button type="button" class="decrease" @click="decreaseQty()">-</button>
<input :name="controlName" class="control" :value="qty" v-validate="'required|numeric|min_value:1'" data-vv-as="&quot;{{ __('shop::app.products.quantity') }}&quot;" readonly>
<button type="button" class="increase" @click="increaseQty()">+</button>
<span class="control-error" v-if="errors.has(controlName)">@{{ errors.first(controlName) }}</span>
</div>
</script>
<script>
Vue.component('product-view', {
@ -128,7 +134,7 @@
data: function() {
return {
is_buy_now: 0
is_buy_now: 0,
}
},
@ -148,13 +154,59 @@
setTimeout(function() {
document.getElementById('product-form').submit();
}, 0);
}
});
}
}
});
Vue.component('quantity-changer', {
template: '#quantity-changer-template',
inject: ['$validator'],
props: {
controlName: {
type: String,
default: 'quantity'
},
quantity: {
type: [Number, String],
default: 1
}
},
data: function() {
return {
qty: this.quantity
}
},
watch: {
quantity: function (val) {
this.qty = val;
this.$emit('onQtyUpdated', this.qty)
}
},
methods: {
decreaseQty: function() {
if (this.qty > 1)
this.qty = parseInt(this.qty) - 1;
this.$emit('onQtyUpdated', this.qty)
},
increaseQty: function() {
this.qty = parseInt(this.qty) + 1;
this.$emit('onQtyUpdated', this.qty)
}
}
});
$(document).ready(function() {
var addTOButton = document.getElementsByClassName('add-to-buttons')[0];
document.getElementById('loader').style.display="none";
@ -196,23 +248,5 @@
}
}
};
function updateQunatity(operation) {
var quantity = document.getElementById('quantity').value;
if (operation == 'add') {
quantity = parseInt(quantity) + 1;
} else if (operation == 'remove') {
if (quantity > 1) {
quantity = parseInt(quantity) - 1;
} else {
alert('{{ __('shop::app.products.less-quantity') }}');
}
}
document.getElementById("quantity").value = quantity;
event.preventDefault();
}
</script>
@endpush

View File

@ -0,0 +1,217 @@
@if ($product->type == 'bundle')
{!! view_render_event('bagisto.shop.products.view.bundle-options.before', ['product' => $product]) !!}
<bundle-option-list></bundle-option-list>
{!! view_render_event('bagisto.shop.products.view.bundle-options.after', ['product' => $product]) !!}
@push('scripts')
<script type="text/x-template" id="bundle-option-list-template">
<div class="bundle-options-wrapper">
<div class="bundle-option-list">
<h3>{{ __('shop::app.products.customize-options') }}</h3>
<bundle-option-item
v-for="(option, index) in options"
:option="option"
:key="index"
:index="index"
@onProductSelected="productSelected(option, $event)">
</bundle-option-item>
</div>
<div class="bundle-summary">
<h3>{{ __('shop::app.products.your-customization') }}</h3>
<quantity-changer></quantity-changer>
<div class="control-group">
<label>{{ __('shop::app.products.total-amount') }}</label>
<div class="bundle-price">
@{{ formated_total_price | currency(currency_options) }}
</div>
</div>
<ul class="bundle-items">
<li v-for="(option, index) in options">
@{{ option.label }}
<div class="selected-products">
<div v-for="(product, index1) in option.products" v-if="product.is_default">
@{{ product.qty + ' x ' + product.name }}
</div>
</div>
</li>
</ul>
</div>
</div>
</script>
<script type="text/x-template" id="bundle-option-item-template">
<div class="bundle-option-item">
<div class="control-group" :class="[errors.has('bundle_options[' + option.id + '][]') ? 'has-error' : '']">
<label class="required">@{{ option.label }}</label>
<div v-if="option.type == 'select'">
<select class="control" :name="'bundle_options[' + option.id + '][]'" v-model="selected_product" v-validate="'required'" :data-vv-as="option.label + '&quot;'">
<option value="">{{ __('shop::app.products.choose-selection') }}</option>
<option v-for="(product, index2) in option.products" :value="product.id">
@{{ product.name + ' + ' + product.price.final_price.formated_price }}
</option>
</select>
</div>
<div v-if="option.type == 'radio'">
<span class="radio" v-for="(product, index2) in option.products">
<input type="radio" :name="'bundle_options[' + option.id + '][]'" v-model="selected_product" v-validate="'required'" :data-vv-as="'&quot;' + option.label + '&quot;'" :value="product.id" :id="'bundle_options[' + option.id + '][]'">
<label class="radio-view" :for="'bundle_options[' + option.id + '][]'"></label>
@{{ product.name }}
<span class="price">
+ @{{ product.price.final_price.formated_price }}
</span>
</span>
</div>
<div v-if="option.type == 'checkbox'">
<span class="checkbox" v-for="(product, index2) in option.products">
<input type="checkbox" :name="'bundle_options[' + option.id + '][]'" :value="product.id" v-model="selected_product" v-validate="'required'" :data-vv-as="'&quot;' + option.label + '&quot;'" :id="'bundle_options[' + option.id + '][]'">
<label class="checkbox-view" :for="'bundle_options[' + option.id + '][]'"></label>
@{{ product.name }}
<span class="price">
+ @{{ product.price.final_price.formated_price }}
</span>
</span>
</div>
<div v-if="option.type == 'multiselect'">
<select class="control" :name="'bundle_options[' + option.id + '][]'" v-model="selected_product" v-validate="'required'" :data-vv-as="'&quot;' + option.label + '&quot;'" multiple>
<option v-for="(product, index2) in option.products" :value="product.id">
@{{ product.name + ' + ' + product.price.final_price.formated_price }}
</option>
</select>
</div>
<span class="control-error" v-if="errors.has('bundle_options[' + option.id + '][]')">
@{{ errors.first('bundle_options[' + option.id + '][]') }}
</span>
</div>
<div v-if="option.type == 'select' || option.type == 'radio'">
<quantity-changer
:control-name="'bundle_option_qty[' + option.id + ']'"
:quantity="product_qty"
@onQtyUpdated="qtyUpdated($event)">
</quantity-changer>
</div>
</div>
</script>
<script>
Vue.component('bundle-option-list', {
template: '#bundle-option-list-template',
inject: ['$validator'],
data: function() {
return {
config: @json(app('Webkul\Product\Helpers\BundleOption')->getBundleConfig($product)),
options: [],
currency_options: @json(core()->getAccountJsSymbols())
}
},
computed: {
formated_total_price: function() {
var total = 0;
for (var key in this.options) {
for (var key1 in this.options[key].products) {
if (! this.options[key].products[key1].is_default)
continue;
total += this.options[key].products[key1].qty * this.options[key].products[key1].price.final_price.price;
}
}
return total;
}
},
created: function() {
for (var key in this.config.options) {
this.options.push(this.config.options[key])
}
},
methods: {
productSelected: function(option, value) {
var selectedProductIds = Array.isArray(value) ? value : [value];
for (var key in option.products) {
option.products[key].is_default = selectedProductIds.indexOf(option.products[key].id) > -1 ? 1 : 0;
}
}
}
});
Vue.component('bundle-option-item', {
template: '#bundle-option-item-template',
props: ['index', 'option'],
inject: ['$validator'],
data: function() {
return {
selected_product: (this.option.type == 'checkbox' || this.option.type == 'multiselect') ? [] : null
}
},
computed: {
product_qty: function() {
return this.option.products[this.selected_product]
? this.option.products[this.selected_product].qty
: 0;
}
},
watch: {
selected_product: function (value) {
this.$emit('onProductSelected', value)
}
},
created: function() {
for (var key1 in this.option.products) {
if (! this.option.products[key1].is_default)
continue;
if (this.option.type == 'checkbox' || this.option.type == 'multiselect') {
this.selected_product.push(this.option.products[key1].id)
} else {
this.selected_product = this.option.products[key1].id
}
}
},
methods: {
qtyUpdated: function(qty) {
if (! this.option.products[this.selected_product])
return;
this.option.products[this.selected_product].qty = qty;
}
}
});
</script>
@endpush
@endif