save quantities

fixed some bugs for update and edit page
pull/44/head
A1Gard 6 months ago
parent dec15afcc3
commit 2cc3475431

@ -393,6 +393,11 @@ function getModelLink($modelable_type, $modelable_id)
}
}
/**
* fix action in log
* @param $act
* @return string
*/
function getAction($act)
{
$r = explode('::', $act);
@ -400,6 +405,10 @@ function getAction($act)
}
/**
* ge all admin routes array
* @return array
*/
function getAdminRoutes()
{
$routes = [];
@ -414,3 +423,20 @@ function getAdminRoutes()
return $routes;
}
/**
* get model with all custom attributes
* @param $model \Illuminate\Database\Eloquent\Model
* @return void
*/
function modelWithCustomAttrs($model){
$data = $model->toArray();
$attrs = $model->getMutatedAttributes();
$attrs = array_diff($attrs,['translations']);
foreach ($attrs as $attr) {
$data[$attr] = $model->getAttribute($attr);
}
return $data;
}

@ -8,6 +8,7 @@ use App\Http\Requests\ProductSaveRequest;
use App\Models\Access;
use App\Models\Category;
use App\Models\Product;
use App\Models\Quantity;
use Illuminate\Http\Request;
use App\Helper;
use function App\Helpers\hasCreateRoute;
@ -26,7 +27,6 @@ class ProductController extends XController
protected $listView = 'admin.products.product-list';
protected $formView = 'admin.products.product-form';
protected $buttons = [
'edit' =>
['title' => "Edit", 'class' => 'btn-outline-primary', 'icon' => 'ri-edit-2-line'],
@ -99,6 +99,30 @@ class ProductController extends XController
// dd($request->input('meta'));
$product->syncMeta(json_decode($request->get('meta','[]'),true));
}
$toRemoveQ = $product->quantities()->pluck('id')->toArray();
if ($request->has('q')){
$qz = json_decode($request->input('q'));
foreach ($qz as $qi){
if ($qi->id == null){
$q = new Quantity();
}else{
$q = Quantity::whereId($qi->id)->first();
unset($toRemoveQ[array_search($q->id, $toRemoveQ) ]); // remove for to remove IDz
}
$q->image = $qi->image;
$q->count = $qi->count;
$q->price = $qi->price;
$q->product_id = $product->id;
$q->data = json_encode($qi->data);
$q->save();
}
$product->quantities()->whereIn('id',$toRemoveQ)->delete();
$product->stock_quantity = $product->quantities()->sum('count');
$product->price = $product->quantities()->min('price');
$product->save();
}
return $product;
@ -121,6 +145,7 @@ class ProductController extends XController
public function edit(Product $item)
{
//
$cats = Category::all(['id','name','parent_id']);
return view($this->formView, compact('item','cats'));
}
@ -168,5 +193,6 @@ class ProductController extends XController
{
return parent::restoreing(Product::withTrashed()->where('id', $item)->first());
}
/*restore**/
}

@ -122,6 +122,7 @@ abstract class XController extends Controller
if ($request->ajax()) {
return ['OK' => true, "message" => __('As you wished created successfully'),
"id" => $item->id,
"data" => modelWithCustomAttrs($item) ,
'url' => getRoute('edit', $item->{$item->getRouteKeyName()})];
} else {
return redirect(getRoute('edit', $item->{$item->getRouteKeyName()}))
@ -150,7 +151,9 @@ abstract class XController extends Controller
logAdmin(__METHOD__, $this->_MODEL_, $item->id);
if ($request->ajax()) {
return ['OK' => true, "message" => __('As you wished updated successfully'), "id" => $item->id];
return ['OK' => true,
"data" => modelWithCustomAttrs($item) ,
"message" => __('As you wished updated successfully'), "id" => $item->id];
} else {
return redirect(getRoute('edit', $item->{$item->getRouteKeyName()}))
->with(['message' => __('As you wished updated successfully')]);

@ -16,6 +16,30 @@ class Product extends Model implements HasMedia
{
use HasFactory, SoftDeletes, InteractsWithMedia, HasTranslations, HasTags, Metable;
protected $casts = [
'qz' => 'array',
'qidz' => 'array'
];
protected $guarded = [];
public function getQzAttribute(){
$result = [];
foreach ($this->quantities as $q) {
if ($q->count > 0){
$q->data = json_decode($q->data);
$result[] = $q;
}
}
return $result;
}
public function getQidzAttribute(){
return $this->quantities()->pluck('id')->toArray();
}
public static $stock_status = ['IN_STOCK', 'OUT_STOCK', 'BACK_ORDER'];
public $translatable = ['name', 'excerpt', 'description','table'];
@ -83,7 +107,7 @@ class Product extends Model implements HasMedia
public function quantities()
{
return $this->hasMany(Quantity::class, 'product_id');
return $this->hasMany(Quantity::class);
}
public function discounts()
@ -148,4 +172,5 @@ class Product extends Model implements HasMedia
}
}

@ -104,3 +104,4 @@ app.mount('#app');
window.app = app;
window.$toast = $toast;
window.store = store;

@ -1,5 +1,14 @@
<template>
<div id="meta-input">
<div id="img-modal" @click.self="modal = false" v-if="modal" class="d-flex align-items-center justify-content-center">
<div class="container">
<div class="row">
<div :class="`col-md-4 `+(j == quantities[qOnEdit].image?'selected-img':'')" v-for="(img,j) in imgz" >
<img :src="img.original_url" @click="changeImgIndex(j)" alt="{{img.id}}" class="img-index">
</div>
</div>
</div>
</div>
<div class="row">
<div v-for="prop in properties" :class="prop.width">
<label for="prop.name" v-if="prop.type != 'checkbox'">
@ -9,7 +18,7 @@
<div v-else class="mt-2">
<br>
</div>
<template v-if="meta[prop.name] != undefined">
<div v-if="meta[prop.name] != undefined" class="position-relative">
<template v-if="prop.type == 'text'">
<input type="text" :id="prop.name" v-model="meta[prop.name]" class="form-control">
</template>
@ -18,14 +27,18 @@
</template>
<template v-if="prop.type == 'checkbox'">
<div class="form-check form-switch">
<input class="form-check-input" v-model="meta[prop.name]" type="checkbox" role="switch" :id="prop.name">
<input class="form-check-input" v-model="meta[prop.name]" type="checkbox" role="switch"
:id="prop.name">
<label class="form-check-label" for="flexSwitchCheckDefault">{{ prop.label }}</label>
</div>
</template>
<template v-if="prop.type == 'color'">
<select :id="prop.name" class="form-control color" v-model="meta[prop.name]">
<option v-for="op in prop.optionList" :style="`background: ${op.value} ;`" :value="op.value"> {{op.title}} </option>
<option v-for="op in prop.optionList" :style="`background: ${op.value} ;`"
:value="op.value"> {{ op.title }}
</option>
</select>
<div class="sq" :style="`background: ${meta[prop.name]} ;`"></div>
</template>
<template v-if="prop.type == 'select' || prop.type == 'singemulti'">
<select :id="prop.name" class="form-control color" v-model="meta[prop.name]">
@ -33,12 +46,99 @@
</select>
</template>
<template v-if="prop.type == 'multi'">
<searchable-multi-select :items="prop.optionList" value-field="value" v-model="meta[prop.name]"></searchable-multi-select>
<searchable-multi-select :items="prop.optionList" value-field="value"
v-model="meta[prop.name]"></searchable-multi-select>
</template>
</div>
</div>
</div>
<div v-if="hasPriceable && productId != null" class="mt-4">
<h4>
Quantities:
<!-- WIP: transalte-->
</h4>
<button type="button" class="btn btn-light w-100 mb-2" @click="addQ">
<i class="ri-add-line"></i>
</button>
<!-- qz: {{ quantitiez }}, qs: {{ quantities }}-->
<div class="row mt-1" v-for="(q,n) in quantities">
<template v-for="prop in properties">
<div v-if="prop.priceable" class="col-md">
<label for="prop.name" v-if="prop.type != 'checkbox'">
{{ prop.label }}
<!-- [{{prop.type}}]-->
</label>
<div v-if="meta[prop.name] != undefined" class="position-relative">
<template v-if="prop.type == 'text'">
<input type="text" :id="prop.name" v-model="q.data[prop.name]" class="form-control">
</template>
<template v-if="prop.type == 'number'">
<input type="number" :id="prop.name" v-model="q.data[prop.name]" class="form-control">
</template>
<template v-if="prop.type == 'checkbox'">
<div class="form-check form-switch">
<input class="form-check-input" v-model="q.data[prop.name]" type="checkbox"
role="switch"
:id="prop.name">
<label class="form-check-label" for="flexSwitchCheckDefault">{{
prop.label
}}</label>
</div>
</template>
<template v-if="prop.type == 'color'">
<select :id="prop.name" class="form-control color" v-model="q.data[prop.name]">
<option v-for="op in prop.optionList" :style="`background: ${op.value} ;`"
:value="op.value"> {{ op.title }}
</option>
</select>
<div class="sq" :style="`background: ${q.data[prop.name]} ;`"></div>
</template>
<template v-if="prop.type == 'select' || prop.type == 'singemulti'">
<select :id="prop.name" class="form-control color" v-model="q.data[prop.name]">
<option v-for="op in prop.optionList" :value="op.value"> {{ op.title }}</option>
</select>
</template>
<template v-if="prop.type == 'multi'">
<searchable-multi-select xname="" :items="prop.optionList" value-field="value"
v-model="q.data[prop.name]"></searchable-multi-select>
</template>
</div>
</div>
</template>
<div class="col-md">
<label :for="`qid-${n}`">
Count: <!-- WIP: transalte-->
</label>
<input type="number" placeholder="Count" :id="`qid-${n}`" min="0" v-model="q.count" class="form-control">
</div>
<div class="col-md">
<label :for="`prc-${n}`">
Price: <!-- WIP: transalte-->
</label>
<currency-input :xid="`qid-${n}`" xtitle="Price" v-model="q.price"></currency-input>
</div>
<div class="col-md" v-if="imgz.length > 0">
<label :for="`img-${n}`">
image: <!-- WIP: transalte-->
</label>
<button type="button" class="btn btn-outline-info d-block w-100" @click="showModal(n)">
<i class="ri-image-2-line"></i>
</button>
</div>
<div class="col-md-1">
<br>
<button type="button" class="btn btn-outline-danger d-block w-100" @click="remQ(n)">
<i class="ri-close-line"></i>
</button>
</div>
</div>
</div>
<!-- {{quantities}}-->
<input type="hidden" name="meta" :value="JSON.stringify(meta)">
<input type="hidden" name="q" :value="JSON.stringify(quantities)">
</div>
</template>
@ -46,27 +146,60 @@
import {mapState} from "vuex";
import searchableMultiSelect from "./SearchableMultiSelect.vue";
import CurrencyInput from "./CurrencyInput.vue";
function arraysEqual(arr1, arr2) {
if (arr1.length !== arr2.length) {
return false;
}
const sortedArr1 = arr1.slice().sort();
const sortedArr2 = arr2.slice().sort();
return sortedArr1.every((value, index) => value === sortedArr2[index]);
}
export default {
name: "meta-input",
components: {
searchableMultiSelect
searchableMultiSelect,
CurrencyInput
},
data: () => {
return {
properties: [],
meta:{}
meta: {},
hasPriceable: false,
quantities: [],
qOnEdit: 0,
modal: false,
lastCat: null,
}
},
props: {
imgz:{
default: []
},
propsApiLink: {
required: true,
},
metaz: {
default: [],
},
quantitiez: {
default: [],
},
productId: {
default: null,
}
},
mounted() {
// this.quantities = this.quantitiez;
for( const q of this.quantitiez) {
q.data = JSON.parse(q.data);
this.quantities.push(q);
}
},
computed: {
category_id: {
@ -77,8 +210,54 @@ export default {
this.$store.commit('UPDATE_CATEGORY', value)
}
},
qsid: {
get() {
return this.$store.state.quantities;
},
set(value) {
// this.$store.commit('UPDATE_CATEGORY', value)
}
},
qid(){
let r = [];
for( const q of this.quantities) {
r.push(q.id);
}
return r;
}
},
methods: {
showModal(i){
console.log('ii',i);
this.qOnEdit = i;
this.modal = true;
},
changeImgIndex(i){
console.log('jjj',i);
this.quantities[this.qOnEdit].image = i;
},
remQ(i){
this.quantities.splice(i,1);
},
addQ() {
let data = {
id: null,
product_id: this.productId,
image: null,
price: 0,
count: 0,
data: {},
};
for (const prop of this.properties) {
// check priceable
if (prop.priceable) {
data.data[prop.name] = '';
}
}
this.quantities.push(data);
},
async updateProps() {
try {
const url = this.propsApiLink + this.category_id;
@ -86,6 +265,10 @@ export default {
this.properties = resp.data.data;
// added don't have
for (const prop of this.properties) {
// check priceable
if (prop.priceable) {
this.hasPriceable = true;
}
if (this.meta[prop.name] == undefined) {
if (prop.type == 'multi') {
this.meta[prop.name] = [];
@ -101,7 +284,6 @@ export default {
}
} catch (e) {
window.$toast.error(e.message);
}
@ -109,8 +291,17 @@ export default {
},
},
watch: {
category_id: function() {
category_id: function (old,n) {
console.log(old,n,'x');
// if (this.lastCat != this.category_id){
// this.lastCat = this.category_id;
this.updateProps();
// }
},
qsid: function () {
if (!arraysEqual(this.qid,this.qsid)){
window.location.href = window.redirect;
}
}
}
}
@ -120,7 +311,41 @@ export default {
#meta-input {
}
.color option {
}
.sq {
width: 37px;
height: 37px;
background: transparent;
border: 1px solid black;
position: absolute;
inset-inline-end: 0;
top: 0;
border-radius: 4px;
}
#img-modal{
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: #00000033;
backdrop-filter: blur(5px);
z-index: 10;
//display: none;
}
.img-index{
width: 100%;
height: 25vh;
min-height: 200px;
object-fit: cover;
}
.selected-img{
background: darkred;
}
</style>

@ -3,16 +3,23 @@ import Vuex from 'vuex';
export default new Vuex.Store({
state: {
category: ''
category: '',
quantities: [],
},
mutations: {
UPDATE_CATEGORY(state, payload) {
state.category = payload;
}
},
UPDATE_QUANTITIES(state, payload) {
state.quantities = payload;
},
},
actions:{
updateCategory(context,cat){
context.commit('UPDATE_CATEGORY',cat);
}
},
updateQuantities(context,idz){
context.commit('UPDATE_QUANTITIES',idz);
},
}
});

@ -98,10 +98,10 @@ document.addEventListener('DOMContentLoaded', () => {
if (res.data.link !== undefined) {
this.setAttribute('action', res.data.link);
}
window.redirect = currentEditLink + res.data.data.slug;
this.setAttribute('action', currentUpdateLink + res.data.data.slug)
$toast.info(res.data.message);
if (document.querySelector('#price-amount').value.trim() !== '') {
window.location.reload();
}
window.store.dispatch('updateQuantities',res.data.data.qidz);
}
}
}).catch(error => {

@ -165,3 +165,9 @@
<br>
@yield('out-of-form')
@endsection
@section('js-content')
<script>
var currentEditLink = '{{route('admin.product.edit','')}}/';
var currentUpdateLink = '{{route('admin.product.update','')}}/';
</script>
@endsection

@ -1,6 +1,12 @@
<meta-input
props-api-link="{{route('v1.category.prop','')}}/"
@if(isset($item))
:metaz='@json($item->getAllMeta())'
:quantitiez='@json($item->quantities)'
product-id="{{$item->id}}"
:imgz='@json($item->getMedia()->toArray())'
@endif
></meta-input>

Loading…
Cancel
Save