added product controller [WIP: discount & meta & quantity]

pull/44/head
A1Gard 6 months ago
parent 62debcbe6d
commit de88d0ce50

@ -359,7 +359,7 @@ function showCatNestedControl($cats, $checked = [], $parent = null)
if ($parent == null) {
return $ret;
} else {
return "<ul> $ret </ul>";
return "<ul class='ps-3'> $ret </ul>";
}
}

@ -18,7 +18,7 @@ class CustomerController extends XController
// protected $SAVE_REQUEST = CustomerSaveRequest::class;
protected $cols = ['name','mobile','email'];
protected $extra_cols = ['id','deleted_at'];
protected $extra_cols = ['id'];
protected $searchable = ['name','mobile','email'];

@ -79,6 +79,8 @@ class PostController extends XController
->toMediaCollection(); //finishing method
}
return $post;
}
@ -90,7 +92,7 @@ class PostController extends XController
public function create()
{
//
$cats = Group::all();
$cats = Group::all(['name','id','parent_id']);
return view($this->formView, compact('cats'));
}
@ -100,7 +102,7 @@ class PostController extends XController
public function edit(Post $item)
{
//
$cats = Group::all();
$cats = Group::all(['name','id','parent_id']);
return view($this->formView, compact('item', 'cats'));
}

@ -0,0 +1,163 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Controllers\XController;
use App\Http\Requests\ProductSaveRequest;
use App\Models\Access;
use App\Models\Category;
use App\Models\Product;
use Illuminate\Http\Request;
use App\Helper;
use function App\Helpers\hasCreateRoute;
class ProductController extends XController
{
// protected $_MODEL_ = Product::class;
// protected $SAVE_REQUEST = ProductSaveRequest::class;
protected $cols = ['name','category_id','view_count','sell_count'];
protected $extra_cols = ['id','slug','image_index'];
protected $searchable = ['name','slug','description','excerpt','sku','table'];
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'],
'show' =>
['title' => "Detail", 'class' => 'btn-outline-light', 'icon' => 'ri-eye-line'],
'destroy' =>
['title' => "Remove", 'class' => 'btn-outline-danger delete-confirm', 'icon' => 'ri-close-line'],
];
public function __construct()
{
parent::__construct(Product::class, ProductSaveRequest::class);
}
/**
* @param $product Product
* @param $request ProductSaveRequest
* @return Product
*/
public function save($product, $request)
{
// dd($request->all());
$product->name = $request->input('name');
$product->slug = $this->getSlug($product,'slug','title');
$product->table = $request->input('table');
$product->description = $request->input('desc');
$product->excerpt = $request->input('excerpt');
$product->stock_status = $request->input('stock_status');
if (!$request->has('quantity')) {
$product->price = $request->input('price',0);
$product->stock_quantity = $request->input('stock_quantity');
}
$product->average_rating = $request->input('average_rating', 0);
$product->average_rating = $request->input('average_rating', 0);
$product->rating_count = $request->input('rating_count', 0);
$product->on_sale = $request->input('on_sale', 1);
$product->sku = $request->input('sku', null);
$product->virtual = $request->input('virtual', false);
$product->downloadable = $request->input('downloadable', false);
$product->category_id = $request->input('category_id');
$product->image_index = $request->input('index_image',0);
$product->user_id = auth()->id();
$product->status = $request->input('status');
$tags = array_filter(explode(',,', $request->input('tags')));
$product->save();
$product->categories()->sync($request->input('cat'));
if (count($tags) > 0){
$product->syncTags($tags);
}
foreach ($product->getMedia() as $media) {
in_array($media->id, request('medias', [])) ?: $media->delete();
}
foreach ($request->file('image', []) as $image) {
try {
$product->addMedia($image)
->preservingOriginal() //middle method
->toMediaCollection(); //finishing method
} catch (FileDoesNotExist $e) {
} catch (FileIsTooBig $e) {
}
}
return $product;
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
$cats = Category::all(['id','name','parent_id']);
return view($this->formView,compact('cats'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Product $item)
{
//
$cats = Category::all(['id','name','parent_id']);
return view($this->formView, compact('item','cats'));
}
public function bulk(Request $request)
{
// dd($request->all());
$data = explode('.', $request->input('action'));
$action = $data[0];
$ids = $request->input('id');
switch ($action) {
case 'delete':
$msg = __(':COUNT items deleted successfully', ['COUNT' => count($ids)]);
$this->_MODEL_::destroy($ids);
break;
/**restore*/
case 'restore':
$msg = __(':COUNT items restored successfully', ['COUNT' => count($ids)]);
foreach ($ids as $id) {
$this->_MODEL_::withTrashed()->find($id)->restore();
}
break;
/*restore**/
default:
$msg = __('Unknown bulk action : :ACTION', ["ACTION" => $action]);
}
return $this->do_bulk($msg, $action, $ids);
}
public function destroy(Product $item)
{
return parent::delete($item);
}
public function update(Request $request, Product $item)
{
return $this->bringUp($request, $item);
}
/**restore*/
public function restore($item)
{
return parent::restoreing(Product::withTrashed()->where('id', $item)->first());
}
/*restore**/
}

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

@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ProductSaveRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return auth()->check();
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'min:5', 'max:128', "unique:products,name," . $this->id],
'sku' => ['nullable', 'string', 'min:1', 'max:128', "unique:products,sku," . $this->id],
'body' => ['nullable', 'string', 'min:5'],
'excerpt' => ['required', 'string', 'min:5'],
'active' => ['nullable', 'boolean'],
'meta' => ['nullable'],
'category_id' => ['required', 'exists:categories,id'],
'image.*' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048'
];
}
}

@ -88,15 +88,16 @@ class Post extends Model implements HasMedia
return $this->morphMany(Comment::class, 'commentable');
}
public function approved_comments()
public function approvedComments()
{
return $this->morphMany(Comment::class, 'commentable')->where('status', 1);
}
public function main_group(){
public function mainGroup(){
return $this->belongsTo(Group::class);
}
// public function toArray()
// {
// return [

@ -4,8 +4,148 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Plank\Metable\Metable;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Spatie\Tags\HasTags;
use Spatie\Translatable\HasTranslations;
class Product extends Model
class Product extends Model implements HasMedia
{
use HasFactory;
use HasFactory, SoftDeletes, InteractsWithMedia, HasTranslations, HasTags, Metable;
public static $stock_status = ['IN_STOCK', 'OUT_STOCK', 'BACK_ORDER'];
public $translatable = ['name', 'excerpt', 'description','table'];
public function registerMediaConversions(?Media $media = null): void
{
$ti = explode('x', config('app.media.product_image'));
if (config('app.media.product_image') == null || config('app.media.product_image') == '') {
$ti[0] = 1200;
$ti[1] = 1200;
}
$t = explode('x', config('app.media.product_thumb'));
if (config('app.media.product_thumb') == null || config('app.media.product_thumb') == '') {
$t[0] = 500;
$t[1] = 500;
}
$this->addMediaConversion('product-thumb')
->width($t[0])
->height($t[1])
->crop($t[0], $t[1])
->optimize()
->sharpen(10)
->nonQueued()
->format('webp');
$this->addMediaConversion('product-image')
->width($ti[0])
->height($ti[1])
->crop($ti[0], $ti[1])
->optimize()
->sharpen(10)
->nonQueued()
->format('webp');
}
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
public function approvedComments()
{
return $this->morphMany(Comment::class, 'commentable')->where('status', 1)->whereNull('sub_comment_id');
}
public function categories()
{
return $this->belongsToMany(Category::class);
}
public function category()
{
return $this->belongsTo(Category::class, 'category_id', 'id');
}
public function getRouteKeyName()
{
return 'slug';
}
public function quantities()
{
return $this->hasMany(Quantity::class, 'product_id');
}
public function discounts()
{
return $this->hasMany(Discount::class, 'product_id', 'id');
}
public function activeDiscounts()
{
return $this->hasMany(Discount::class, 'product_id', 'id')->where(function ($query) {
$query->where('expire', '>=', date('Y-m-d'))
->orWhereNull('expire');
});
}
public function quesions()
{
return $this->hasMany(Question::class);
}
function hasDiscount()
{
return $this->discounts()->where('expire', '>', date('Y-m-d'))->count() > 0;
}
public function isFav()
{
if (auth('customer')->check()) {
return \auth('customer')->user()->products()->where('product_id', $this->id)->exists();
} else {
return false;
}
}
public function imgUrl(){
if ($this->getMedia()->count() > 0) {
return $this->getMedia()[$this->image_index]->getUrl();
} else {
return asset('assets/upload/logo.svg');
}
}
public function imgUrl2(){
if ($this->getMedia()->count() > 0 && isset($this->getMedia()[1])) {
return $this->getMedia()[1]->getUrl();
} else {
return asset('assets/upload/logo.svg');
}
}
public function thumbUrl(){
if ($this->getMedia()->count() > 0) {
return $this->getMedia()[$this->image_index]->getUrl('product-thumb');
} else {
return asset('assets/upload/logo.svg');
}
}
public function thumbUrl2(){
if ($this->getMedia()->count() > 0 && isset($this->getMedia()[1])) {
return $this->getMedia()[1]->getUrl('product-thumb');
} else {
return asset('assets/upload/logo.svg');
}
}
}

@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Question extends Model
{
// use HasFactory;
public function product(){
return $this->belongsTo(Product::class);
}
public function customer(){
return $this->belongsTo(Customer::class);
}
}

@ -147,6 +147,8 @@ return [
'media' => [
'gallery_thumb' => env('MEDIA_GALLEY_THUMB','500x500'),
'post_thumb' => env('MEDIA_POST_THUMB','500x500'),
'product_thumb' => env('MEDIA_PRODUCT_THUMB','500x500'),
'product_image' => env('MEDIA_PRODUCT_IMAGE','1200x1200'),
],

@ -16,6 +16,7 @@ return new class extends Migration
$table->text('name');
$table->string('slug')->unique()->index();
$table->longText('description')->nullable();
$table->longText('table')->nullable();
$table->text('excerpt')->nullable()->comment('Quick summary for product. This will appear on the product page under the product name and for SEO purpose.');
$table->string('sku')->nullable()->unique()->comment('SKU refers to a Stock-keeping unit, a unique identifier for each distinct product and service that can be purchased.');
$table->boolean('virtual')->nullable()->default(false)->index()->comment('If this product is a non-physical item, for example a service, which does not need shipping.');
@ -25,11 +26,11 @@ return new class extends Migration
$table->unsignedBigInteger('user_id');
$table->boolean('on_sale')->nullable()->default(true)->index();
$table->unsignedBigInteger('stock_quantity')->nullable()->default(0);
$table->enum('stock_status',['IN_STOCK','OUT_STOCK','BACK_ORDER'])->nullable()->default('IN_STOCK')->index();
$table->enum('stock_status',\App\Models\Product::$stock_status)->nullable()->default(\App\Models\Product::$stock_status[0])->index();
$table->unsignedBigInteger('rating_count')->nullable()->default(0);
$table->decimal('average_rating',3,2)->unsigned()->nullable()->default(0.00);
$table->unsignedBigInteger('total_sales')->nullable()->default(0);
$table->boolean('active')->default(true);
// $table->unsignedBigInteger('total_sales')->nullable()->default(0);
$table->unsignedTinyInteger('status')->default(0);
$table->unsignedBigInteger('view_count')->default(0);
$table->unsignedBigInteger('sell_count')->default(0);
$table->unsignedTinyInteger('image_index')->default(0);

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('questions', function (Blueprint $table) {
$table->id();
$table->text('body');
$table->unsignedBigInteger('customer_id');
$table->text('answer')->default(null)->nullable();
$table->unsignedBigInteger('product_id');
$table->tinyInteger('status')->default(0);
$table->timestamps();
$table->foreign('product_id')->on('products')
->references('id')->onDelete('cascade');
$table->foreign('customer_id')->on('customers')
->references('id')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('questions');
}
};

@ -2,6 +2,7 @@
namespace Database\Seeders;
use App\Models\Category;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
@ -13,5 +14,46 @@ class CategorySeeder extends Seeder
public function run(): void
{
//
$mainCategories = [
__("Mobile"),
__("Tablet"),
__("Desktop"),
__("Brands"),
];
$subCats = [
1 => [
__("Smart phone"),
__("Basic phones"),
],
3 => [
__("PC"),
__("Laptop"),
],
4 => [
__("Apple"),
__("HP (Hewlett-Packard)"),
__("Nokia"),
__("Samsung"),
__("Sony"),
],
];
// insert main categories
foreach ($mainCategories as $category){
$c = new Category();
$c->name = $category;
$c->slug = sluger($category);
$c->save();
}
foreach ($subCats as $k => $categories){
foreach ($categories as $category){
$c = new Category();
$c->name = $category;
$c->slug = sluger($category);
$c->parent_id = $k ;
$c->save();
}
}
}
}

@ -31,6 +31,7 @@ class DatabaseSeeder extends Seeder
PostSeeder::class,
StateSeeder::class,
CustomerSeeder::class,
CategorySeeder::class,
]
);
}

@ -19,6 +19,8 @@ import './panel/navbar.js';
import './panel/list-checkboxs.js';
import './panel/general-events.js';
import './panel/editor-handle.js';
import './panel/step-controller.js';
import './panel/product-upload-controller.js';
/**
* Next, we will create a fresh Vue application instance. You may then begin

@ -10,10 +10,16 @@
<template v-for="(item,i) in items">
<li
tabindex="-1"
v-if="(q != '' && item[titleField].toLocaleLowerCase().indexOf(q.toLocaleLowerCase()) != -1) || (q == '')"
v-if="finder(item[titleField])"
@click="selecting(item[valueField])"
:class="`list-group-item ${val.indexOf(item[valueField]) !== -1?'selected':''} ${focsed == i?'focused':''}`">
{{ item[titleField] }}
<template v-if="xlang == null">
{{ item[titleField] }}
</template>
<template v-else>
{{ item[titleField][xlang] }}
</template>
</li>
</template>
</ul>
@ -29,7 +35,12 @@
<div class="form-control" id="vue-lst" @click.self="showModal">
<template v-for="item in items">
<span class="tag-select" v-if=" val.indexOf(item[valueField]) !== -1">
{{ item[titleField] }}
<template v-if="xlang == null">
{{ item[titleField] }}
</template>
<template v-else>
{{ item[titleField][xlang] }}
</template>
<i class="ri-close-line" @click="rem(item[valueField])"></i>
</span>
</template>
@ -54,6 +65,9 @@ export default {
},
emits: ['update:modelValue'],
props: {
xlang: {
default: null
},
modelValue: {
default: 'nop',
},
@ -118,6 +132,29 @@ export default {
},
},
methods: {
finder(term = '') {
//(q != '' && item[titleField].indexOf(q) != -1) || (q == '')
if (this.q == '' || term == '') {
return true;
}
if (typeof term == 'string' && term.toLocaleLowerCase().indexOf(this.q.toLocaleLowerCase()) != -1) {
return true
} else if (typeof term == 'object') {
try {
for (const t in term) {
if (term[t].toLowerCase().indexOf(this.q.toLocaleLowerCase()) != -1) {
return true;
}
}
} catch (e) {
console.log(e.message);
}
} else {
return true;
}
return false;
},
rem(i) {
this.val.splice(this.val.indexOf(i), 1);
this.onSelect(this.val, i);

@ -8,29 +8,39 @@
<div class="p-2">
<ul id="vue-search-list" class="list-group list-group-flush">
<template v-for="item in items">
<li
v-if="(q != '' && item[titleField].indexOf(q) != -1) || (q == '')"
@click="selecting(item[valueField])"
:class="`list-group-item ${val == item[valueField]?'selected':''}`">
{{item[titleField]}}
</li>
<li
v-if="finder(item[titleField])"
@click="selecting(item[valueField])"
:class="`list-group-item ${val == item[valueField]?'selected':''}`">
<template v-if="xlang == null">
{{ item[titleField] }}
</template>
<template v-else>
{{ item[titleField][xlang] }}
</template>
</li>
</template>
</ul>
</div>
</div>
</div>
<div class="input-group mb-3">
<div class="input-group-prepend" id="vue-search-btn" @click="showModal" >
<div class="input-group-prepend" id="vue-search-btn" @click="showModal">
<span class="input-group-text" id="basic-addon1">
<i class="ri-search-2-line"></i>
</span>
</div>
<select :id="xid" :class="getClass" v-model="val" @change="select">
<option value=""> {{xtitle}} </option>
<select :id="xid" :class="getClass" v-model="val" @change="select">
<option value=""> {{ xtitle }}</option>
<option v-for="item in items"
:value="item[valueField]"
:selected="item[valueField] == val">
{{item[titleField]}}
<template v-if="xlang == null">
{{ item[titleField] }}
</template>
<template v-else>
{{ item[titleField][xlang] }}
</template>
</option>
</select>
</div>
@ -46,53 +56,56 @@ export default {
return {
modalShow: false, // modal handle
q: '', // search query
val:'',
val: '',
}
},
emits: ['update:modelValue'],
props: {
xlang: {
default: null
},
modelValue: {
default: NaN,
},
items:{
items: {
required: true,
default: [],
type: Array,
},
valueField:{
valueField: {
default: 'id',
type: String,
},
titleField:{
titleField: {
default: 'title',
type: String,
},
xname:{
xname: {
default: "",
type: String,
},
xtitle:{
xtitle: {
default: "Please select",
type: String,
},
xvalue:{
xvalue: {
default: "",
type: String,
},
xid:{
xid: {
default: "",
type: String,
},
customClass:{
customClass: {
default: "",
type: String,
},
err:{
err: {
default: false,
type: Boolean,
},
onSelect:{
onSelect: {
default: function () {
},
@ -106,33 +119,56 @@ export default {
mounted() {
if (!isNaN(this.modelValue)) {
this.val = this.modelValue;
}else{
} else {
this.val = this.xvalue;
}
},
computed: {
getClass: function () {
if (this.err == true || ( typeof this.err == 'String' && this.err.trim() == '1' )) {
return 'form-control is-invalid '+this.customClass;
if (this.err == true || (typeof this.err == 'String' && this.err.trim() == '1')) {
return 'form-control is-invalid ' + this.customClass;
}
return 'form-control '+this.customClass;
return 'form-control ' + this.customClass;
},
},
methods: {
selecting(i){
finder(term = '') {
//(q != '' && item[titleField].indexOf(q) != -1) || (q == '')
if (this.q == '' || term == '') {
return true;
}
if (typeof term == 'string' && term.toLocaleLowerCase().indexOf(this.q.toLocaleLowerCase()) != -1) {
return true
} else if (typeof term == 'object') {
try {
for (const t in term) {
if (term[t].toLowerCase().indexOf(this.q.toLocaleLowerCase()) != -1) {
return true;
}
}
} catch (e) {
console.log(e.message);
}
} else {
return true;
}
return false;
},
selecting(i) {
this.val = i;
this.onSelect(this.val);
if (this.closeOnSelect){
if (this.closeOnSelect) {
this.hideModal();
}
},
select(){
select() {
this.onSelect(this.val);
},
hideModal:function () {
hideModal: function () {
this.modalShow = false;
},
showModal(){
showModal() {
this.modalShow = true;
}
},
@ -151,17 +187,17 @@ export default {
}
#vue-search-btn{
#vue-search-btn {
cursor: pointer;
user-select: none;
}
#vue-search-btn:hover .input-group-text{
#vue-search-btn:hover .input-group-text {
background: darkred;
}
#ss-modal{
#ss-modal {
position: fixed;
left: 0;
right: 0;
@ -173,7 +209,7 @@ export default {
user-select: none;
}
#ss-selector{
#ss-selector {
height: 60vh;
border-radius: 7px;
min-width: 350px;
@ -184,15 +220,16 @@ export default {
padding: 5px;
}
#vue-search-list{
#vue-search-list {
height: calc(60vh - 90px);
overflow-x: auto ;
overflow-x: auto;
}
#vue-search-list .list-group-item:hover{
#vue-search-list .list-group-item:hover {
background: deepskyblue;
}
#vue-search-list .list-group-item.selected{
#vue-search-list .list-group-item.selected {
background: dodgerblue;
color: white;;
}

@ -5,7 +5,7 @@
{{$tag}}
<i class="ri-close-line" @click="rem($tag)"></i>
</span>
<input type="text" v-model="tag"
<input type="text" v-model="tag" :id="xid"
@keyup.enter.prevent="addTag"
@focus="disableSubmit"
@blur="enableSubmit" :placeholder="xtitle">
@ -30,6 +30,9 @@ export default {
},
emits: ['update:modelValue'],
props: {
xid:{
default: 'tags',
},
modelValue: {
default: null,
},
@ -77,9 +80,11 @@ export default {
},
disableSubmit(e){
e.target.closest('form').addEventListener('submit',noSubmit);
window.noSubmit = true;
},
enableSubmit(e){
e.target.closest('form').removeEventListener('submit',noSubmit);
window.noSubmit = false;
},
rem(tag){
this.tags.splice(this.tags.indexOf(tag),1);
@ -91,7 +96,7 @@ export default {
<style scoped>
#tag-input {
padding: .5rem;
padding: .5rem 0;
}
#tag-input input{

@ -0,0 +1,193 @@
var isW8 = false;
let uploadFormData = [];
let xTimer;
window.noSubmit = false;
function previewImage(input, i) {
try {
const oFReader = new FileReader();
oFReader.readAsDataURL(input);
oFReader.onload = function (oFREvent) {
const img = oFREvent.target.result;
const uploadingImages = document.querySelector('#uploading-images');
const newDiv = document.createElement('div');
newDiv.dataset.id = i;
newDiv.className = 'col-xl-3 col-md-4 border p-3';
newDiv.innerHTML = `
<div class="img-preview" style="background-image: url('${img}')"></div>
<div class="btn btn-danger upload-remove-image d-block">
<span class="ri-close-line"></span>
</div>
`;
uploadingImages.appendChild(newDiv);
};
if (xTimer !== undefined) {
clearTimeout(xTimer);
}
xTimer = setTimeout(() => {
document.querySelectorAll('.img-preview').forEach(el => {
el.style.height = `${el.offsetWidth}px`;
});
window.dispatchEvent(new Event('resize'));
}, 300);
} catch (e) {
console.error('Error in previewImage:', e);
}
}
document.addEventListener('DOMContentLoaded', () => {
const uploadingImages = document.querySelector('#uploading-images');
const uploadDragDrop = document.querySelector('#upload-drag-drop');
const uploadImageSelect = document.querySelector('#upload-image-select');
const indexImage = document.querySelector('#index-image');
document.querySelector('.product-form')?.addEventListener('submit', function(e) {
e.preventDefault();
if (isW8 || window.noSubmit) {
return false;
}
const formData = new FormData(this);
let j = 0;
for (const f of uploadFormData) {
if (uploadFormData.length == j) {
break;
}
j++;
try {
if (f.size === undefined) {
continue;
}
} catch (e) {
console.log(e.message);
continue;
}
console.log('x',f);
formData.append('image[]', f);
}
const submitButtons = document.querySelectorAll("[type='submit']");
submitButtons.forEach(button => {
button.disabled = true;
button.classList.add('w8');
});
isW8 = true;
const url = this.getAttribute('action');
axios({
method: 'post',
url: url,
data: formData,
headers: {'Content-Type': 'multipart/form-data'}
}).then(res => {
submitButtons.forEach(button => {
button.disabled = false;
button.classList.remove('w8');
});
isW8 = false;
if (res.data.OK) {
if (res.data.url !== undefined) {
window.location.href = res.data.url;
} else {
if (res.data.link !== undefined) {
this.setAttribute('action', res.data.link);
}
if (document.querySelector('#price-amount').value.trim() !== '') {
window.location.reload();
}
}
}
}).catch(error => {
document.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
submitButtons.forEach(button => {
button.disabled = false;
button.classList.remove('w8');
});
isW8 = false;
for (let i in error.response.data.errors) {
document.getElementById(i)?.classList.add('is-invalid');
for (const err of error.response.data.errors[i]) {
// alertify.error(err);
console.log(err);
}
}
});
});
uploadingImages.addEventListener('dblclick', (e) => {
const imageIndex = e.target.closest('.image-index');
if (imageIndex) {
document.querySelectorAll('.indexed').forEach(el => el.classList.remove('indexed'));
imageIndex.classList.add('indexed');
indexImage.value = imageIndex.dataset.key;
}
});
document.querySelectorAll('.img-preview').forEach(el => {
el.style.height = `${el.offsetWidth}px`;
});
uploadDragDrop?.addEventListener('click', () => {
uploadImageSelect.click();
});
uploadImageSelect?.addEventListener('change', () => {
for (const file of uploadImageSelect.files) {
console.log(file);
uploadFormData.push(file);
previewImage(file, uploadFormData.length);
}
});
document.addEventListener('click', (e) => {
if (e.target.closest('.upload-remove-image')) {
const parentCol = e.target.closest('.col-md-4');
const dataId = parentCol.dataset.id;
delete uploadFormData[dataId - 1];
parentCol.style.transition = 'opacity 400ms';
parentCol.style.opacity = '0';
setTimeout(() => parentCol.remove(), 400);
}
});
uploadDragDrop?.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
uploadDragDrop.classList.add('active');
});
['dragenter', 'dragstart'].forEach(eventName => {
uploadDragDrop?.addEventListener(eventName, (e) => {
e.preventDefault();
e.stopPropagation();
uploadDragDrop.classList.add('active');
});
});
['dragleave', 'dragend'].forEach(eventName => {
uploadDragDrop?.addEventListener(eventName, (e) => {
e.preventDefault();
e.stopPropagation();
uploadDragDrop.classList.remove('active');
});
});
uploadDragDrop?.addEventListener('drop', (e) => {
uploadDragDrop.classList.remove('active');
if (e.dataTransfer && e.dataTransfer.files.length) {
e.preventDefault();
e.stopPropagation();
for (const f of e.dataTransfer.files) {
previewImage(f, uploadFormData.length);
uploadFormData.push(f);
}
}
});
});

@ -0,0 +1,59 @@
document.addEventListener('DOMContentLoaded', function() {
const steps = document.querySelectorAll('.steps li');
const stepsContainer = document.querySelector('.steps');
const stepTabs = document.querySelectorAll('#step-tabs > div');
const nextButtons = document.querySelectorAll('.step-next');
const prevButtons = document.querySelectorAll('.step-prev');
let currentStep = 0;
function updateProgress(stepIndex) {
const progress = (stepIndex + 1) / steps.length * 100;
stepsContainer?.style.setProperty('--progress', `${progress}%`);
}
function showStep(stepIndex) {
steps.forEach((step, index) => {
step.classList.toggle('active', index <= stepIndex);
});
stepTabs.forEach((tab, index) => {
if (index === stepIndex) {
tab.classList.add('active');
setTimeout(() => tab.style.opacity = 1, 0);
} else {
tab.style.opacity = 0;
setTimeout(() => tab.classList.remove('active'), 0);
}
});
updateProgress(stepIndex);
currentStep = stepIndex;
}
function nextStep() {
if (currentStep < steps.length - 1) {
showStep(currentStep + 1);
}
}
function prevStep() {
if (currentStep > 0) {
showStep(currentStep - 1);
}
}
steps.forEach((step, index) => {
step.addEventListener('click', () => showStep(index));
});
nextButtons.forEach(button => {
button.addEventListener('click', nextStep);
});
prevButtons.forEach(button => {
button.addEventListener('click', prevStep);
});
// Show the first step initially
showStep(0);
});

@ -32,6 +32,9 @@
padding: 0;
margin: 0;
}
html {
scroll-behavior: smooth;
}
body{
min-height: 100vh;
overflow-x: hidden;
@ -65,3 +68,4 @@ a{
@import "panel/navbar";
@import "panel/breadcrumbs";
@import "panel/item-list";
@import "panel/steps";

@ -82,8 +82,9 @@ a.btn,a.action-btn,a.circle-btn{
list-style: none;
overflow-y: auto;
padding: 0 .5rem;
height: 12rem;
ul{
padding: 0 .5rem;
list-style: none;
}
input{
margin: 0 .5rem;

@ -0,0 +1,113 @@
.steps {
// ... other styles ...
--progress: 0%;
display: flex;
justify-content: space-around;
list-style-type: none;
padding: 1rem 0;
position: relative;
&:before {
content: ' ';
position: absolute;
top: 50%;
left: 0;
right: 0;
border-bottom: 1px solid white;
}
&:after {
content: ' ';
position: absolute;
top: 50%;
inset-inline-start: 0;
inset-inline-end: calc(100% - var(--progress));
border-bottom: 1px solid lighten($primary-color-panel,20);
transition: all 0.3s ease-in-out;
}
li {
position: relative;
z-index: 2;
background: $lighter-color;
cursor: pointer;
border: 1px solid #ccc;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
width: 65px;
height: 65px;
i {
font-size: 35px;
}
&.active {
border-color: lighten($primary-color-panel,20);
background-color: $primary-color-panel;
}
}
}
#step-tabs {
> div {
display: none;
opacity: 0;
//transition: opacity 0.3s ease-in-out;
&.active {
display: block;
opacity: 1;
}
}
}
.product-form .card-footer{
display: flex;
justify-content: space-around;
flex-direction: row-reverse;
}
#upload-drag-drop {
border: 2px dashed silver;
min-height: 150px;
padding: 20px 10px;
text-align: center;
margin: 10px auto;
transition: 600ms;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
&:hover {
border-color: $primary-color-panel;
}
}
#upload-drag-drop.active {
border-color: $primary-color-panel;
}
.img-preview{
background-size: cover;
}
.img-list{
object-fit: cover;
width: 100%;
height: 275px;
}
.indexed{
background: $primary-color-panel;
}
#upload-image-select{
display: none;
}

@ -0,0 +1,167 @@
@extends('layouts.app')
@section('title')
@if(isset($item))
{{__("Edit product")}} [{{$item->id}}]
@else
{{__("Add new product")}}
@endif -
@endsection
@section('content')
@if(hasRoute('create') && isset($item))
<a class="action-btn circle-btn"
data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-custom-class="custom-tooltip"
data-bs-title="{{__("Add another one")}}"
href="{{getRoute('create')}}"
>
<i class="ri-add-line"></i>
</a>
@else
<a class="action-btn circle-btn"
data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-custom-class="custom-tooltip"
data-bs-title="{{__("Show list")}}"
href="{{getRoute('index',[])}}"
>
<i class="ri-list-view"></i>
</a>
@endif
<form
@if(isset($item))
id="product-form-edit"
action="{{getRoute('update',$item->{$item->getRouteKeyName()})}}"
@else
id="product-form-create"
action="{{getRoute('store')}}"
@endif
class="product-form pb-5 pb-5"
method="post" enctype="multipart/form-data">
@csrf
@if(isset($item))
<input type="hidden" name="id" value="{{$item->id}}"/>
@endif
<ul class="steps">
<li data-tab="step1"
data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-custom-class="custom-tooltip"
data-bs-title="{{__("Basic data")}}">
<i class="ri-pages-line"></i>
</li>
<li data-tab="step2"
data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-custom-class="custom-tooltip"
data-bs-title="{{__("Medias")}}">
<i class="ri-image-2-line"></i>
</li>
<li data-tab="step3"
data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-custom-class="custom-tooltip"
data-bs-title="{{__("Additional data")}}">
<i class="ri-list-check-2"></i>
</li>
<li data-tab="step4"
data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-custom-class="custom-tooltip"
data-bs-title="{{__("Publish")}}">
<i class="ri-save-3-line"></i>
</li>
</ul>
<div id="step-tabs">
<div id="step1">
<div class="card">
<div class="card-header">
{{__("Basic data")}}
</div>
<div class="card-body">
@include('admin.products.sub-pages.product-step1')
</div>
<div class="card-footer">
<button type="submit" class="btn btn-light">
{{__("Publish")}}
</button>
<button type="button" class="btn btn-light step-next">
{{__("Next")}}
</button>
</div>
</div>
</div>
<div id="step2">
<div class="card">
<div class="card-header">
{{__("Medias")}}
</div>
<div class="card-body">
@include('admin.products.sub-pages.product-step2')
</div>
<div class="card-footer">
<!-- code -->
<button type="submit" class="btn btn-light">
{{__("Publish")}}
</button>
<button type="button" class="btn btn-light step-next">
{{__("Next")}}
</button>
<button type="button" class="btn btn-light step-prev">
{{__("Previous")}}
</button>
</div>
</div>
</div>
<div id="step3">
<div class="card">
<div class="card-header">
{{__("Additional data")}}
</div>
<div class="card-body">
@include('admin.products.sub-pages.product-step3')
</div>
<div class="card-footer">
<!-- code -->
<button type="submit" class="btn btn-light">
{{__("Publish")}}
</button>
<button type="button" class="btn btn-light step-next">
{{__("Next")}}
</button>
<button type="button" class="btn btn-light step-prev">
{{__("Previous")}}
</button>
</div>
</div>
</div>
<div id="step4">
<div class="card">
<div class="card-header">
{{__("Publish")}}
</div>
<div class="card-body">
@include('admin.products.sub-pages.product-step4')
</div>
<div class="card-footer">
<!-- code -->
<button type="submit" class="btn btn-light">
{{__("Publish")}}
</button>
<button type="button" class="btn btn-light step-prev">
{{__("Previous")}}
</button>
</div>
</div>
</div>
</div>
</form>
<br>
<br>
@yield('out-of-form')
@endsection

@ -0,0 +1,30 @@
@extends('admin.templates.panel-list-template')
@section('list-title')
<i class="ri-user-3-line"></i>
{{__("Products list")}}
@endsection
@section('title')
{{__("Products list")}} -
@endsection
@section('filter')
{{-- Other filters --}}
<h2>
<i class="ri-book-3-line"></i>
{{__("Category")}}:
</h2>
<searchable-multi-select
:items='{{\App\Models\Category::all(['id','name'])}}'
title-field="name"
value-field="id"
xlang="{{config('app.locale')}}"
xname="filter[category_id]"
:xvalue='{{request()->input('filter.category_id','[]')}}'
:close-on-Select="true"></searchable-multi-select>
@endsection
@section('bulk')
{{-- <option value="-"> - </option> --}}
<option value="publish"> {{__("Publish")}} </option>
<option value="draft"> {{__("Draft")}} </option>
@endsection

@ -0,0 +1,107 @@
<div class="row">
<div class="col-md-6 mt-3">
<div class="form-group">
<label for="name">
{{__('Name')}}
</label>
<input name="name" type="text"
id="name"
class="form-control @error('name') is-invalid @enderror"
placeholder="{{__('Name')}}"
value="{{old('name',$item->name??null)}}"/>
</div>
</div>
<div class="col-md-6 mt-3">
<div class="form-group">
<label for="name">
{{__('Slug')}}
</label>
<input name="slug" type="text"
id="slug"
class="form-control @error('slug') is-invalid @enderror"
placeholder="{{__('Slug')}}"
value="{{old('slug',$item->slug??null)}}"/>
</div>
</div>
<div class="col-lg-3 mt-3">
<div class="form-group">
<label for="price">
{{__('Base price')}}
</label>
<currency-input name="price" xid="price" @error('price')
:err="true" @enderror :xvalue="{{old('price',$item->price??null)}}"></currency-input>
</div>
</div>
<div class="col-lg-3 mt-3">
<div class="form-group">
<label for="categoryId">
{{__('Main product category')}}1
</label>
{{-- data-url="{{route('props.list','')}}/"--}}
<searchable-select
@error('category_id') :err="true" @enderror
:items='@json($cats)'
title-field="name"
value-field="id"
xlang="{{config('app.locale')}}"
xid="categoryId"
xname="category_id"
@error('category_id') :err="true" @enderror
xvalue='{{old('category_id',$item->category_id??null)}}'
:close-on-Select="true"></searchable-select>
</div>
</div>
<div class="col-lg-3 mt-3">
<div class="form-group">
<label for="price">
{{__('SKU')}}
</label>
<input name="sku" type="text"
id="sku"
class="form-control @error('sku') is-invalid @enderror"
placeholder="{{__('SKU')}}"
value="{{old('sku',$item->sku??null)}}"/>
</div>
</div>
<div class="col-lg-3 mt-3">
<div class="form-group">
<label for="status">
{{__('Status')}}
</label>
<select name="status" id="status"
class="form-control @error('status') is-invalid @enderror">
<option value="1"
@if (old('status',$item->status??null) == '1' ) selected @endif >{{__("Published")}} </option>
<option value="0"
@if (old('status',$item->status??null) == '0' ) selected @endif >{{__("Draft")}} </option>
</select>
</div>
</div>
<div class="col-md-12 mt-3">
<div class="form-group">
<label for="excerpt">
{{__('Excerpt')}}
</label>
<textarea name="excerpt"
class="form-control @error('excerpt') is-invalid @enderror"
placeholder="{{__('Excerpt')}}"
id="excerpt"
rows="4">{{old('excerpt',$item->excerpt??null)}}</textarea>
</div>
</div>
<div class="col-md-12 mt-3">
<div class="form-group">
<label for="description">
{{__('Description Text')}}
</label>
<textarea name="desc" class="form-control ckeditorx @error('description') is-invalid @enderror"
placeholder="{{__('Description Text')}}"
id="description"
rows="8">{{old('description',$item->description??null)}}</textarea>
</div>
</div>
</div>

@ -0,0 +1,30 @@
<div class="alert alert-info pt-4">
<p>&check; &nbsp; {{__("The first and/or second image will be index image")}} <br>
&check; &nbsp; {{__("You can choose one or more image together")}} <br>
&check; &nbsp; {{__("Double click on image to change index image")}}
</p>
</div>
<div class="uploader-images">
<input type="file" multiple accept=".jpg,.png,.gif" id="upload-image-select"/>
</div>
<div id="upload-drag-drop">
<h2>
{{__("Click here to upload or drag and drop here")}}
</h2>
</div>
<div id="uploading-images" class="row">
@if (isset($item))
@foreach($item->getMedia() as $k => $media)
<div data-id="-1" data-key="{{$k}}"
class="image-index col-xl-3 col-md-4 border p-3 @if($k == $item->image_index) indexed @endif">
<img class="img-list" src="{{$media->getUrl()}}" alt="{{$k}}">
<div class="btn btn-danger upload-remove-image d-block">
<span class="ri-close-line"></span>
</div>
<input type="hidden" name="medias[]" value="{{$media->id}}"/>
</div>
@endforeach
<input type="hidden" name="index_image" id="index-image" value="{{$item?->image_index}}">
@endif
</div>

@ -0,0 +1,177 @@
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="stock_quantity" class="my-2">
{{__('Stock quantity')}}
</label>
<input type="number" id="stock_quantity" name="stock_quantity"
value="{{old('stock_quantity',$item->stock_quantity??0)}}"
placeholder="{{__('Stock quantity')}}"
class="form-control">
</div>
<div class="form-group">
<label for="status" class="my-2">
{{__("Status")}}
</label>
<select class="form-control" name="stock_status" id="status">
@foreach(\App\Models\Product::$stock_status as $k => $v)
<option
value="{{ $k }}" {{ old("stock_status", $item->stock_status??null) == $k ? "selected" : "" }}>{{ __($v) }}</option>
@endforeach
</select>
</div>
<label for="tags" class="mt-2">
{{__("Tags")}}
</label>
<tag-input xname="tags" splitter=",," xid="tags"
xtitle="{{__("Tags, Press enter")}}"
@if(isset($item))
xvalue="{{old('title',implode(',,',$item->tags->pluck('name')->toArray()??''))}}"
@endif
></tag-input>
</div>
<div class="col-md-6">
<h5>
{{__("Categories")}}
</h5>
<ul class="group-control">
{!!showCatNestedControl($cats,old('cat',isset($item)?$item->categories()->pluck('id')->toArray():[]))!!}
</ul>
</div>
</div>
<div>
<div class="form-group">
<label for="table">
{{__('Description Table')}}
</label>
<textarea name="table" class="ckeditorx @error('description') is-invalid @enderror"
placeholder="{{__('Description Table')}}"
id="table"
rows="8">{{old('table',$item->table??null)}}</textarea>
</div>
</div>
<div class="accordion mt-2" id="accordionExample">
<div class="accordion-item">
<h2 class="accordion-header" id="headingTwo">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
{{__("Discounts")}}
</button>
</h2>
<div id="collapseTwo" class="accordion-collapse collapse" aria-labelledby="headingTwo"
data-bs-parent="#accordionExample">
<div class="accordion-body">
<table class="table" id="discounts">
<tr>
<th>
{{__("Type")}}
</th>
<th>
{{__("Amount")}}
</th>
<th>
{{__("Discount code")}}
</th>
<th>
{{__("Expire date")}}
</th>
<th>
-
</th>
</tr>
@if(isset($item))
@foreach($item->discounts as $dis)
<tr>
<td>
{{$dis->type}}
</td>
<td>
{{$dis->amount}}
</td>
<td>
{{$dis->code}}
</td>
<td>
{{$dis->expire->jdate('Y/m/d')}}
</td>
<td>
<button type="button" class="btn btn-danger" data-id="{{$dis->id}}">
<span class="ri-close-line"></span>
</button>
</td>
</tr>
@endforeach
@endif
</table>
<input type="hidden" id="discount-rem" name="discount[remove]" value="[]">
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingThree">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
{{__("New Discount")}}
</button>
</h2>
<div id="collapseThree" class="accordion-collapse collapse" aria-labelledby="headingThree"
data-bs-parent="#accordionExample">
<div class="accordion-body">
<table class="table" id="new-discount">
<thead>
<tr>
<th>
{{__("Type")}}
</th>
<th>
{{__("Amount")}}
</th>
{{-- <th>--}}
{{-- {{__("Discount code")}}--}}
{{-- </th>--}}
<th>
{{__("Expire date")}}
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<label>
{{__("by price")}}
<input type="radio" checked name="discount[type]" value="price">
</label>
&nbsp;
&nbsp;
<label>
{{__("by percent")}}
<input type="radio" name="discount[type]" value="percent">
</label>
</td>
<td>
<input type="text" id="price-amount" placeholder="{{__("Amount")}}"
name="discount[amount]" class="form-control">
</td>
{{-- <td>--}}
{{-- <input type="text" placeholder="{{__("Discount code")}}" name="discount[code]" class="form-control">--}}
{{-- </td>--}}
<td>
<input placeholder="{{__("Expire date")}}" type="text" data-reuslt="#exp-date"
class="form-control dtp">
<input type="hidden" name="discount[expire]" id="exp-date">
</td>
<td>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>

@ -40,7 +40,7 @@
</a>
<ul id="catalog">
<li>
<a>
<a href="{{route('admin.product.index')}}">
<i class="ri-vip-diamond-fill"></i>
{{__('Products')}}
</a>

@ -129,6 +129,19 @@ Route::prefix(config('app.panel.prefix'))->name('admin.')->group(
Route::get('delete/{item}', [\App\Http\Controllers\Admin\GalleryController::class, 'destroy'])->name('destroy');
Route::post('bulk', [\App\Http\Controllers\Admin\GalleryController::class, "bulk"])->name('bulk');
});
Route::prefix('products')->name('product.')->group(
function () {
Route::get('', [\App\Http\Controllers\Admin\ProductController::class, 'index'])->name('index');
Route::get('create', [\App\Http\Controllers\Admin\ProductController::class, 'create'])->name('create');
Route::post('store', [\App\Http\Controllers\Admin\ProductController::class, 'store'])->name('store');
Route::get('show/{item}', [\App\Http\Controllers\Admin\ProductController::class, 'show'])->name('show');
Route::post('title/update', [\App\Http\Controllers\Admin\ProductController::class, 'updateTitle'])->name('title');
Route::get('edit/{item}', [\App\Http\Controllers\Admin\ProductController::class, 'edit'])->name('edit');
Route::post('update/{item}', [\App\Http\Controllers\Admin\ProductController::class, 'update'])->name('update');
Route::get('delete/{item}', [\App\Http\Controllers\Admin\ProductController::class, 'destroy'])->name('destroy');
Route::post('bulk', [\App\Http\Controllers\Admin\ProductController::class, "bulk"])->name('bulk');
});
Route::prefix('sliders')->name('slider.')->group(
function () {
Route::get('', [\App\Http\Controllers\Admin\SliderController::class, 'index'])->name('index');

Loading…
Cancel
Save