Merged with master

This commit is contained in:
prashant-webkul 2018-09-12 12:27:01 +05:30
commit 4e7b1f157b
19 changed files with 1570 additions and 1429 deletions

View File

@ -96,6 +96,12 @@ class EventServiceProvider extends ServiceProvider
Event::listen('admin.acl.build', function ($acl) {
$acl->add('dashboard', 'Dashboard', 'admin.dashboard.index', 1);
$acl->add('catalog', 'Catalog', 'admin.catalog.index', 2);
$acl->add('catalog.products', 'Products', 'admin.catalog.products.index', 1);
$acl->add('catalog.categories', 'Categories', 'admin.catalog.categories.index', 1);
$acl->add('configuration', 'Configure', 'admin.account.edit', 5);
$acl->add('settings', 'Settings', 'admin.users.index', 6);

View File

@ -134,6 +134,22 @@ class Product extends Model
return $this->attribute_family->custom_attributes->pluck('code')->contains($attribute);
}
/**
* @param string $key
*
* @return bool
*/
public function isSaleable()
{
if($this->status) {
if($this->inventories->sum('qty')) {
return true;
}
}
return false;
}
/**
* Get an attribute from the model.
*

View File

@ -0,0 +1,238 @@
<?php
namespace Webkul\Product\Product;
use Webkul\Attribute\Repositories\AttributeOptionRepository as AttributeOption;
use Webkul\Product\Product\Gallery;
use Webkul\Product\Product\Price;
class ConfigurableOption extends AbstractProduct
{
/**
* AttributeOptionRepository object
*
* @var array
*/
protected $attributeOption;
/**
* Gallery object
*
* @var array
*/
protected $gallery;
/**
* Price object
*
* @var array
*/
protected $price;
/**
* Create a new controller instance.
*
* @param Webkul\Attribute\Repositories\AttributeOptionRepository $attributeOption
* @param Webkul\Product\Product\Gallery $gallery
* @param Webkul\Product\Product\Price $price
* @return void
*/
public function __construct(
AttributeOption $attributeOption,
Gallery $gallery,
Price $price
)
{
$this->attributeOption = $attributeOption;
$this->gallery = $gallery;
$this->price = $price;
}
/**
* Returns the allowed variants
*
* @param Product $product
* @return float
*/
public function getAllowProducts($product)
{
$variants = [];
foreach ($product->variants as $variant) {
if ($variant->isSaleable()) {
$variants[] = $variant;
}
}
return $variants;
}
/**
* Returns the allowed variants JSON
*
* @param Product $product
* @return float
*/
public function getConfigurationConfig($product)
{
$options = $this->getOptions($product, $this->getAllowProducts($product));
$config = [
'attributes' => $this->getAttributesData($product, $options),
'index' => isset($options['index']) ? $options['index'] : [],
'regular_price' => [
'formated_price' => core()->currency($this->price->getMinimalPrice($product)),
'price' => $this->price->getMinimalPrice($product)
],
'variant_prices' => $this->getVariantPrices($product),
'variant_images' => $this->getVariantImages($product),
'chooseText' => trans('shop::app.products.choose-option')
];
return $config;
}
/**
* Get allowed attributes
*
* @param Product $product
* @return array
*/
public function getAllowAttributes($product)
{
return $product->super_attributes;
}
/**
* Get Configurable Product Options
*
* @param Product $currentProduct
* @param array $allowedProducts
* @return array
*/
public function getOptions($currentProduct, $allowedProducts)
{
$options = [];
$allowAttributes = $this->getAllowAttributes($currentProduct);
foreach ($allowedProducts as $product) {
$productId = $product->id;
foreach ($allowAttributes as $productAttribute) {
$productAttributeId = $productAttribute->id;
$attributeValue = $product->{$productAttribute->code};
$options[$productAttributeId][$attributeValue][] = $productId;
$options['index'][$productId][$productAttributeId] = $attributeValue;
}
}
return $options;
}
/**
* Get product attributes
*
* @param Product $product
* @param array $options
* @return array
*/
public function getAttributesData($product, array $options = [])
{
$defaultValues = [];
$attributes = [];
foreach ($product->super_attributes as $attribute) {
$attributeOptionsData = $this->getAttributeOptionsData($attribute, $options);
if ($attributeOptionsData) {
$attributeId = $attribute->id;
$attributes[] = [
'id' => $attributeId,
'code' => $attribute->code,
'label' => $attribute->name,
'options' => $attributeOptionsData
];
}
}
return $attributes;
}
/**
* @param Attribute $attribute
* @param array $options
* @return array
*/
protected function getAttributeOptionsData($attribute, $options)
{
$attributeOptionsData = [];
foreach ($attribute->options as $attributeOption) {
$optionId = $attributeOption->id;
if(isset($options[$attribute->id][$optionId])) {
$attributeOptionsData[] = [
'id' => $optionId,
'label' => $attributeOption->label,
'products' => $options[$attribute->id][$optionId]
];
}
}
return $attributeOptionsData;
}
/**
* Get product prices for configurable variations
*
* @param Product $product
* @return array
*/
protected function getVariantPrices($product)
{
$prices = [];
foreach ($this->getAllowProducts($product) as $variant) {
$prices[$variant->id] = [
'regular_price' => [
'formated_price' => core()->currency($variant->price),
'price' => $variant->price
],
'final_price' => [
'formated_price' => core()->currency($this->price->getMinimalPrice($variant)),
'price' => $this->price->getMinimalPrice($variant)
]
];
}
return $prices;
}
/**
* Get product images for configurable variations
*
* @param Product $product
* @return array
*/
protected function getVariantImages($product)
{
$images = [];
foreach ($this->getAllowProducts($product) as $variant) {
$images[$variant->id] = $this->gallery->getImages($variant);
}
return $images;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Webkul\Product\Product;
use Webkul\Attribute\Repositories\AttributeOptionRepository as AttributeOption;
class Gallery extends AbstractProduct
{
/**
* Retrieve collection of gallery images
*
* @param Product $product
* @return array
*/
public function getImages($product)
{
$images[] = [
'small_image_url' => '',
'medium_image_url' => '',
'large_image_url' => '',
];
return $images;
}
}

View File

@ -78,7 +78,7 @@ class Price extends AbstractProduct
public function getSpecialPrice($product)
{
if($this->haveSpecialPrice($product)) {
return $product->special_price;
return $product->special_price;
} else {
return $product->price;
}

View File

@ -4,6 +4,17 @@ namespace Webkul\Product\Product;
class Review extends AbstractProduct
{
/**
* Returns the product's avg rating
*
* @param Product $product
* @return float
*/
public function getReviews($product)
{
return $product->reviews()->where('status', 'approved');
}
/**
* Returns the product's avg rating
*
@ -12,7 +23,7 @@ class Review extends AbstractProduct
*/
public function getAverageRating($product)
{
return round($product->reviews->average('rating'));
return number_format(round($product->reviews()->where('status', 'approved')->average('rating'), 2), 1);
}
/**
@ -23,7 +34,7 @@ class Review extends AbstractProduct
*/
public function getTotalReviews($product)
{
return $product->reviews()->count();
return $product->reviews()->where('status', 'approved')->count();
}
/**

File diff suppressed because it is too large Load Diff

View File

@ -49,6 +49,9 @@ return [
'specification' => 'Specification',
'total-reviews' => ':total Reviews',
'by' => 'By :name',
'up-sell-title' => 'We found other products you might like!'
'up-sell-title' => 'We found other products you might like!',
'reviews-title' => 'Ratings & Reviews',
'write-review-btn' => 'Write Review',
'choose-option' => 'Choose an option'
]
];

View File

@ -21,12 +21,12 @@
<div id="app">
@include('shop::layouts.header.index')
@yield('slider')
<div class="main-container-wrapper">
@include('shop::layouts.header.index')
@yield('slider')
<div class="content-container">
@yield('content-wrapper')

View File

@ -22,11 +22,7 @@
@include ('shop::products.price', ['product' => $product])
@if ($product->reviews->count())
@include ('shop::products.review', ['product' => $product])
@endif
@include ('shop::products.review', ['product' => $product])
@include ('shop::products.add-to', ['product' => $product])

View File

@ -6,7 +6,7 @@
<span class="price-label">{{ __('shop::app.products.price-label') }}</span>
<span>{{ core()->currency($priceHelper->getMinimalPrice($product)) }}</span>
<span class="final-price">{{ core()->currency($priceHelper->getMinimalPrice($product)) }}</span>
@else

View File

@ -1,11 +1,19 @@
<div class="product-ratings">
@inject ('reviewHelper', 'Webkul\Product\Product\Review')
@if ($total = $reviewHelper->getTotalReviews($product))
@inject ('reviewHelper', 'Webkul\Product\Product\Review')
<div class="product-ratings">
@for ($i = 1; $i <= $reviewHelper->getAverageRating($product); $i++)
<span class="stars">
@for ($i = 1; $i <= round($reviewHelper->getAverageRating($product)); $i++)
<span class="icon star-icon"></span>
@endfor
</div>
<span class="icon star-icon"></span>
@endfor
</span>
<div class="total-reviews">
{{ __('shop::app.products.total-reviews', ['total' => $total]) }}
</div>
</div>
@endif

View File

@ -9,129 +9,57 @@
</div>
<div class="layouter">
@include ('shop::products.view.gallery')
<form method="POST" @auth('customer') action="{{ route('cart.customer.add', $product->id) }}" @endauth @guest action="{{ route('cart.guest.add', $product->id) }}" @endguest>
@csrf()
<div class="product-details" id="dealit">
<input type="hidden" name="product">
<div class="product-heading">
<span>{{ $product->name }}</span>
</div>
@include ('shop::products.view.gallery')
<div class="rating">
<img src="{{ bagisto_asset('images/5star.svg') }}" />
75 Ratings & 11 Reviews
</div>
<div class="details">
@include ('shop::products.price', ['product' => $product])
<div class="product-heading">
<span>{{ $product->name }}</span>
</div>
@include ('shop::products.view.stock')
@include ('shop::products.review', ['product' => $product])
<br/>
@include ('shop::products.price', ['product' => $product])
<div class="description">
{{ $product->short_description }}
</div>
@include ('shop::products.view.stock')
@if ($product->type == 'configurable')
<div class="description">
{{ $product->short_description }}
</div>
@include ('shop::products.view.configurable-options')
@endif
<accordian :title="{{ __('shop::app.products.description') }}" :active="true">
<div slot="header">
{{ __('shop::app.products.description') }}
<i class="icon expand-icon right"></i>
</div>
<div slot="body">
<div class="full-description">
{{ $product->description }}
<accordian :title="'{{ __('shop::app.products.description') }}'" :active="true">
<div slot="header">
{{ __('shop::app.products.description') }}
<i class="icon expand-icon right"></i>
</div>
</div>
</accordian>
@include ('shop::products.view.attributes')
<div slot="body">
<div class="full-description">
{{ $product->description }}
</div>
</div>
</accordian>
@include ('shop::products.view.reviews')
@include ('shop::products.view.attributes')
@include ('shop::products.view.reviews')
</div>
</form>
</div>
</div>
@include ('shop::products.view.up-sells')
</section>
@push('scripts')
<script type="text/javascript">
var topBoundOfProductGallery = 0;
function getTopBound() {
var rect = document.getElementById("getbound").getBoundingClientRect();
topBoundOfProductGallery = rect.top;
console.log('From Top = ', rect.top);
}
window.onload = getTopBound;
// window.onscroll = function() {
// myFunction()
// };
// $(document).ready(function () {
// $(document).scroll(function (event) {
// var scroll = $(document).scrollTop();
// if(scroll > 182) {
// $('#dealit').css('width', '50%');
// $('#dealit').css('margin-left', '59.7%');
// $('#getbound').css('position', 'fixed');
// $('#getbound').css('top', '0');
// } else if(scroll < 182) {
// $('#dealit').css('width', '100%');
// $('#dealit').css('margin-left', '');
// $('#getbound').css('position', '');
// $('#getbound').css('top', '');
// }
// });
// });
// function myFunction() {
// if(document.body.scrollTop > 182 || document.documentElement.scrollTop > 182) {
// // document.getElementById('dealit').style.style = 'none';
// document.getElementById('dealit').classList.remove("product-details");
// document.getElementById('dealit').width = '50%';
// document.getElementById('dealit').marginLeft = '60%';
// document.getElementById('getbound').style.position = 'fixed';
// document.getElementById('getbound').style.top = '0';
// }
// else if(document.body.scrollTop < 182 || document.documentElement.scrollTop < 182) {
// document.getElementById('dealit').classList.add("product-details");
// document.getElementById('dealit').width = '100%';
// document.getElementById('dealit').marginLeft = '0';
// document.getElementById('getbound').style.position = '';
// document.getElementById('getbound').style.top = '';
// }
// }
</script>
@endpush
@endsection

View File

@ -1,6 +1,6 @@
@inject ('productViewHelper', 'Webkul\Product\Product\View')
<accordian :title="{{ __('shop::app.products.specification') }}" :active="false">
<accordian :title="'{{ __('shop::app.products.specification') }}'" :active="false">
<div slot="header">
{{ __('shop::app.products.specification') }}
<i class="icon expand-icon right"></i>

View File

@ -1,33 +1,240 @@
<div class="attributes">
@if ($product->type == 'configurable')
<div class="attribute color">
<div class="title">Color</div>
@inject ('configurableOptionHelper', 'Webkul\Product\Product\ConfigurableOption')
<div class="values">
<div class="colors red"></div>
<div class="colors blue"></div>
<div class="colors green"></div>
</div>
</div>
<product-options></product-options>
<div class="attribute size">
<div class="title">Size</div>
@push('scripts')
<div class="values">
<div class="size xl">XL</div>
<div class="size xxl">XXL</div>
<div class="size xxxl">XXXL</div>
</div>
</div>
<script type="text/x-template" id="product-options-template">
<div class="attributes">
<div class="attribute quantity">
<div class="title">Quantity</div>
<input type="hidden" name="selected_configurable_option" :value="selectedProductId">
<div class="values">
<div class="size">1</div>
</div>
</div>
<div v-for='(attribute, index) in childAttributes' class="attribute control-group" :class="[errors.has('super_attribute[' + attribute.id + ']') ? 'has-error' : '']">
<label class="reqiured">@{{ attribute.label }}</label>
</div>
<select v-validate="'required'" class="control" :name="['super_attribute[' + attribute.id + ']']" :disabled="attribute.disabled" @change="configure(attribute, $event.target.value)" :id="['attribute_' + attribute.id]">
<hr/>
<option v-for='(option, index) in attribute.options' :value="option.id">@{{ option.label }}</option>
</select>
<span class="control-error" v-if="errors.has('super_attribute[' + attribute.id + ']')">
@{{ errors.first('super_attribute[' + attribute.id + ']') }}
</span>
</div>
</div>
</script>
<?php $config = $configurableOptionHelper->getConfigurationConfig($product) ?>
<script>
Vue.component('product-options', {
template: '#product-options-template',
data: () => ({
config: @json($config),
childAttributes: [],
selectedProductId: '',
simpleProduct: null
}),
created () {
var config = @json($config);
var childAttributes = this.childAttributes,
attributes = config.attributes.slice(),
index = attributes.length,
attribute;
while (index--) {
attribute = attributes[index];
attribute.options = [];
if (index) {
attribute.disabled = true;
} else {
this.fillSelect(attribute);
}
attribute = Object.assign(attribute, {
childAttributes: childAttributes.slice(),
prevAttribute: attributes[index - 1],
nextAttribute: attributes[index + 1]
});
childAttributes.unshift(attribute);
}
},
methods: {
configure (attribute, value) {
this.simpleProduct = this.getSelectedProductId(attribute, value);
if (value) {
attribute.selectedIndex = this.getSelectedIndex(attribute, value);
if (attribute.nextAttribute) {
attribute.nextAttribute.disabled = false;
this.fillSelect(attribute.nextAttribute);
this.resetChildren(attribute.nextAttribute);
} else {
this.selectedProductId = attribute.options[attribute.selectedIndex].allowedProducts[0];
}
} else {
attribute.selectedIndex = 0;
this.resetChildren(attribute);
this.clearSelect(attribute.nextAttribute)
}
this.reloadPrice();
this.changeProductImages();
},
getSelectedIndex (attribute, value) {
var selectedIndex = 0;
attribute.options.forEach(function(option, index) {
if(option.id == value) {
selectedIndex = index;
}
})
return selectedIndex;
},
getSelectedProductId (attribute, value) {
var options = attribute.options,
matchedOptions;
matchedOptions = options.filter(function (option) {
return option.id == value;
});
if(matchedOptions[0] != undefined && matchedOptions[0].allowedProducts != undefined) {
return matchedOptions[0].allowedProducts[0];
}
return undefined;
},
fillSelect (attribute) {
var options = this.getAttributeOptions(attribute.id),
prevOption,
index = 1,
allowedProducts,
i,
j;
this.clearSelect(attribute)
attribute.options = [];
attribute.options[0] = {'id': '', 'label': this.config.chooseText, 'products': []};
if (attribute.prevAttribute) {
prevOption = attribute.prevAttribute.options[attribute.prevAttribute.selectedIndex];
}
if (options) {
for (i = 0; i < options.length; i++) {
allowedProducts = [];
if (prevOption) {
for (j = 0; j < options[i].products.length; j++) {
if (prevOption.products && prevOption.products.indexOf(options[i].products[j]) > -1) {
allowedProducts.push(options[i].products[j]);
}
}
} else {
allowedProducts = options[i].products.slice(0);
}
if (allowedProducts.length > 0) {
options[i].allowedProducts = allowedProducts;
attribute.options[index] = options[i];
index++;
}
}
}
},
resetChildren (attribute) {
if (attribute.childAttributes) {
attribute.childAttributes.forEach(function (set) {
set.selectedIndex = 0;
set.disabled = true;
});
}
},
clearSelect: function (attribute) {
if(!attribute)
return;
var element = document.getElementById("attribute_" + attribute.id);
if(element) {
element.selectedIndex = "0";
}
},
getAttributeOptions (attributeId) {
var this_this = this,
options;
this.config.attributes.forEach(function(attribute, index) {
if (attribute.id == attributeId) {
options = attribute.options;
}
})
return options;
},
reloadPrice () {
var selectedOptionCount = 0;
this.childAttributes.forEach(function(attribute) {
if(attribute.selectedIndex) {
selectedOptionCount++;
}
});
var priceLabelElement = document.querySelector('.price-label');
var priceElement = document.querySelector('.final-price');
if(this.childAttributes.length == selectedOptionCount) {
priceLabelElement.style.display = 'none';
priceElement.innerHTML = this.config.variant_prices[this.simpleProduct].final_price.formated_price;
} else {
priceLabelElement.style.display = 'inline-block';
priceElement.innerHTML = this.config.regular_price.formated_price;
}
},
changeProductImages () {
console.log(this.config.variant_images[this.simpleProduct])
},
}
});
</script>
@endpush
@endif

View File

@ -1,4 +1,4 @@
<div class="product-gallery-group" id="getbound">
<div class="product-gallery-group">
<div class="product-image-group">
<div class="side-group">
@ -8,24 +8,19 @@
<img src="{{ bagisto_asset('images/jeans.jpg') }}" />
</div>
<div class="product-hero-image">
<img src="{{ bagisto_asset('images/jeans_big.jpg') }}" />
<div class="product-hero-image" id="product-hero-image">
<img class="hero-image" src="{{ bagisto_asset('images/jeans_big.jpg') }}" />
<img class="wishlist" src="{{ bagisto_asset('images/wish.svg') }}" />
<img class="share" src="{{ bagisto_asset('images/icon-share.svg') }}" />
</div>
</div>
<div class="product-button-group">
<form method="POST" @auth('customer') action="{{ route('cart.customer.add', $product->id) }}" @endauth @guest action="{{ route('cart.guest.add', $product->id) }}" @endguest>
{{ csrf_field() }}
<input type="hidden" name="product_id" value="{{ $product->id }}">
<input type="hidden" name="product_id" value="{{ $product->id }}">
<input type="hidden" name="qty" value="1">
<input type="hidden" name="qty" value="1">
<input type="submit" class="btn btn-lg add-to-cart" value="Add to Cart">
</form>
<input type="submit" class="btn btn-lg add-to-cart" value="Add to Cart">
{{-- <form>
<input type="hidden" name="product_id" value="">
<input type="hidden" name="qty" value="1">
@ -34,3 +29,14 @@
{{-- {{ dd(unserialize(Cookie::get('session_c'))) }} --}}
</div>
</div>
@push('scripts')
<script>
Vue.component('product-gallery', {
props: ['images']
});
</script>
@endpush

View File

@ -2,8 +2,8 @@
@if ($total = $reviewHelper->getTotalReviews($product))
<div class="rating-reviews">
<div class="title">
Ratings & Reviews
<div class="rating-header">
{{ __('shop::app.products.reviews-title') }}
</div>
<div class="overall">
@ -14,7 +14,7 @@
</span>
<span class="stars">
@for ($i = 1; $i <= $reviewHelper->getAverageRating($product); $i++)
@for ($i = 1; $i <= round($reviewHelper->getAverageRating($product)); $i++)
<span class="icon star-icon"></span>
@ -27,13 +27,15 @@
</div>
<a href="{{ route('shop.reviews.create', $product->url_key) }}" class="btn btn-lg btn-primary">Write Review</a>
<a href="{{ route('shop.reviews.create', $product->url_key) }}" class="btn btn-lg btn-primary">
{{ __('shop::app.products.write-review-btn') }}
</a>
</div>
<div class="reviews">
@foreach ($product->reviews()->paginate(5) as $review)
@foreach ($reviewHelper->getReviews($product)->paginate(5) as $review)
<div class="review">
<div class="title">
{{ $review->title }}
@ -65,7 +67,6 @@
<a href="{{ route('shop.reviews.index', $product->url_key) }}" class="view-all">View All</a>
<hr/>
</div>
</div>
@endif

File diff suppressed because it is too large Load Diff

View File

@ -4,4 +4,4 @@
{{ Illuminate\Mail\Markdown::parse($slot) }}
</td>
</tr>
</table>
</table>