diff --git a/app/Helpers/Helper.php b/app/Helpers/Helper.php index c764d9d..fa7d8f2 100644 --- a/app/Helpers/Helper.php +++ b/app/Helpers/Helper.php @@ -359,7 +359,7 @@ function showCatNestedControl($cats, $checked = [], $parent = null) if ($parent == null) { return $ret; } else { - return ""; + return ""; } } diff --git a/app/Http/Controllers/Admin/CustomerController.php b/app/Http/Controllers/Admin/CustomerController.php index c97c5e0..22a9edc 100644 --- a/app/Http/Controllers/Admin/CustomerController.php +++ b/app/Http/Controllers/Admin/CustomerController.php @@ -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']; diff --git a/app/Http/Controllers/Admin/PostController.php b/app/Http/Controllers/Admin/PostController.php index 91aa1ef..0dd3f86 100644 --- a/app/Http/Controllers/Admin/PostController.php +++ b/app/Http/Controllers/Admin/PostController.php @@ -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')); } diff --git a/app/Http/Controllers/Admin/ProductController.php b/app/Http/Controllers/Admin/ProductController.php new file mode 100644 index 0000000..5247261 --- /dev/null +++ b/app/Http/Controllers/Admin/ProductController.php @@ -0,0 +1,163 @@ + + ['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**/ +} diff --git a/app/Http/Controllers/XController.php b/app/Http/Controllers/XController.php index 4fb0a56..f2af53c 100644 --- a/app/Http/Controllers/XController.php +++ b/app/Http/Controllers/XController.php @@ -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')]); diff --git a/app/Http/Requests/ProductSaveRequest.php b/app/Http/Requests/ProductSaveRequest.php new file mode 100644 index 0000000..2a27d03 --- /dev/null +++ b/app/Http/Requests/ProductSaveRequest.php @@ -0,0 +1,35 @@ +check(); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|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' + ]; + } +} diff --git a/app/Models/Post.php b/app/Models/Post.php index 54908fb..6934c27 100644 --- a/app/Models/Post.php +++ b/app/Models/Post.php @@ -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 [ diff --git a/app/Models/Product.php b/app/Models/Product.php index c428a88..c4f6678 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -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'); + } + } + + } diff --git a/app/Models/Question.php b/app/Models/Question.php new file mode 100644 index 0000000..466b6a2 --- /dev/null +++ b/app/Models/Question.php @@ -0,0 +1,17 @@ +belongsTo(Product::class); + } + public function customer(){ + return $this->belongsTo(Customer::class); + } +} diff --git a/config/app.php b/config/app.php index 9a691f7..bb6ec38 100644 --- a/config/app.php +++ b/config/app.php @@ -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'), ], diff --git a/database/migrations/2024_05_07_130016_create_products_table.php b/database/migrations/2024_05_07_130016_create_products_table.php index 36379ff..863281b 100644 --- a/database/migrations/2024_05_07_130016_create_products_table.php +++ b/database/migrations/2024_05_07_130016_create_products_table.php @@ -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); diff --git a/database/migrations/2024_06_19_200943_create_questions_table.php b/database/migrations/2024_06_19_200943_create_questions_table.php new file mode 100644 index 0000000..8f3c53c --- /dev/null +++ b/database/migrations/2024_06_19_200943_create_questions_table.php @@ -0,0 +1,37 @@ +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'); + } +}; diff --git a/database/seeders/CategorySeeder.php b/database/seeders/CategorySeeder.php index bc3c0e2..0702828 100644 --- a/database/seeders/CategorySeeder.php +++ b/database/seeders/CategorySeeder.php @@ -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(); + } + } } } diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 39a9bcc..60e312c 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -31,6 +31,7 @@ class DatabaseSeeder extends Seeder PostSeeder::class, StateSeeder::class, CustomerSeeder::class, + CategorySeeder::class, ] ); } diff --git a/resources/js/app.js b/resources/js/app.js index 6b55882..db54e23 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -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 diff --git a/resources/js/components/SearchableMultiSelect.vue b/resources/js/components/SearchableMultiSelect.vue index c602eef..bd01689 100644 --- a/resources/js/components/SearchableMultiSelect.vue +++ b/resources/js/components/SearchableMultiSelect.vue @@ -10,10 +10,16 @@ @@ -29,7 +35,12 @@
@@ -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); diff --git a/resources/js/components/SearchableSelect.vue b/resources/js/components/SearchableSelect.vue index f6cd9ce..0af601a 100644 --- a/resources/js/components/SearchableSelect.vue +++ b/resources/js/components/SearchableSelect.vue @@ -8,29 +8,39 @@
-
+
- +
@@ -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;; } diff --git a/resources/js/components/TagInput.vue b/resources/js/components/TagInput.vue index 39c1d97..13d38c4 100644 --- a/resources/js/components/TagInput.vue +++ b/resources/js/components/TagInput.vue @@ -5,7 +5,7 @@ {{$tag}} - @@ -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 {