|
@@ -0,0 +1,343 @@
|
|
|
|
|
+{!! view_render_event('bagisto.admin.catalog.categories.edit.card.products.before', ['category' => $category]) !!}
|
|
|
|
|
+
|
|
|
|
|
+<!-- Products -->
|
|
|
|
|
+<div class="box-shadow rounded bg-white p-4 dark:bg-gray-900">
|
|
|
|
|
+ <p class="mb-4 flex items-center justify-between text-base font-semibold text-gray-800 dark:text-white">
|
|
|
|
|
+ @lang('admin::app.catalog.categories.edit.products')
|
|
|
|
|
+ </p>
|
|
|
|
|
+
|
|
|
|
|
+ <v-category-products>
|
|
|
|
|
+ <x-admin::shimmer.datagrid />
|
|
|
|
|
+ </v-category-products>
|
|
|
|
|
+</div>
|
|
|
|
|
+
|
|
|
|
|
+{!! view_render_event('bagisto.admin.catalog.categories.edit.card.products.after', ['category' => $category]) !!}
|
|
|
|
|
+
|
|
|
|
|
+@pushOnce('scripts')
|
|
|
|
|
+ <script
|
|
|
|
|
+ type="text/x-template"
|
|
|
|
|
+ id="v-category-products-template"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <!-- Header actions -->
|
|
|
|
|
+ <div class="mb-4 flex justify-end">
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="secondary-button"
|
|
|
|
|
+ @click="openModal"
|
|
|
|
|
+ >
|
|
|
|
|
+ @lang('admin::app.catalog.categories.edit.add-product')
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <template v-if="isLoading">
|
|
|
|
|
+ <x-admin::shimmer.datagrid />
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ <!-- Current category products (sortable) -->
|
|
|
|
|
+ <div v-if="products.length" class="mb-6">
|
|
|
|
|
+ <draggable
|
|
|
|
|
+ ghost-class="draggable-ghost"
|
|
|
|
|
+ v-bind="{ animation: 200 }"
|
|
|
|
|
+ handle=".icon-drag"
|
|
|
|
|
+ :list="products"
|
|
|
|
|
+ item-key="id"
|
|
|
|
|
+ @end="onDragEnd"
|
|
|
|
|
+ >
|
|
|
|
|
+ <template #item="{ element }">
|
|
|
|
|
+ <div class="flex items-center justify-between gap-3 border-b border-slate-200 py-3 dark:border-gray-800">
|
|
|
|
|
+ <div class="flex items-center gap-3">
|
|
|
|
|
+ <span class="icon-drag cursor-move text-xl text-gray-400"></span>
|
|
|
|
|
+
|
|
|
|
|
+ <img
|
|
|
|
|
+ v-if="element.base_image_url"
|
|
|
|
|
+ :src="element.base_image_url"
|
|
|
|
|
+ class="h-10 w-10 rounded object-cover"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p class="text-sm font-medium text-gray-800 dark:text-white" v-text="element.name"></p>
|
|
|
|
|
+
|
|
|
|
|
+ <p class="text-xs text-gray-500 dark:text-gray-400" v-text="element.sku"></p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="flex items-center gap-3">
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ :value="element.position"
|
|
|
|
|
+ class="w-20 rounded-md border px-2 py-1 text-sm text-gray-600 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300"
|
|
|
|
|
+ @change="updatePosition(element, $event)"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="transparent-button !p-1.5 text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-950"
|
|
|
|
|
+ @click="removeProduct(element.id)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="icon-delete text-xl"></span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </draggable>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-else
|
|
|
|
|
+ class="py-6 text-center text-sm text-gray-500 dark:text-gray-400"
|
|
|
|
|
+ >
|
|
|
|
|
+ @lang('admin::app.catalog.categories.edit.no-products')
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Sub-category products (read-only) -->
|
|
|
|
|
+ <div v-if="childrenProducts.length">
|
|
|
|
|
+ <p class="mb-3 text-sm font-semibold text-gray-700 dark:text-gray-200">
|
|
|
|
|
+ @lang('admin::app.catalog.categories.edit.child-category-products')
|
|
|
|
|
+ </p>
|
|
|
|
|
+
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="product in childrenProducts"
|
|
|
|
|
+ :key="'child-' + product.id"
|
|
|
|
|
+ class="flex items-center gap-3 border-b border-slate-200 py-3 dark:border-gray-800"
|
|
|
|
|
+ >
|
|
|
|
|
+ <img
|
|
|
|
|
+ v-if="product.base_image_url"
|
|
|
|
|
+ :src="product.base_image_url"
|
|
|
|
|
+ class="h-10 w-10 rounded object-cover"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p class="text-sm font-medium text-gray-800 dark:text-white" v-text="product.name"></p>
|
|
|
|
|
+
|
|
|
|
|
+ <p class="text-xs text-gray-500 dark:text-gray-400">
|
|
|
|
|
+ <span v-text="product.sku"></span>
|
|
|
|
|
+ <span class="mx-1">·</span>
|
|
|
|
|
+ <span class="text-gray-400" v-text="product.category_name"></span>
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Add Product Modal -->
|
|
|
|
|
+ <x-admin::modal ref="addProductModal">
|
|
|
|
|
+ <x-slot:header>
|
|
|
|
|
+ <p class="text-lg font-bold text-gray-800 dark:text-white">
|
|
|
|
|
+ @lang('admin::app.catalog.categories.edit.add-product')
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </x-slot>
|
|
|
|
|
+
|
|
|
|
|
+ <x-slot:content>
|
|
|
|
|
+ <div class="mb-4 flex gap-2">
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ v-model="searchQuery"
|
|
|
|
|
+ class="flex min-h-[39px] w-full rounded-md border px-3 py-2 text-sm text-gray-600 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300"
|
|
|
|
|
+ placeholder="@lang('admin::app.catalog.categories.edit.search-products')"
|
|
|
|
|
+ @keyup.enter="searchProducts"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="secondary-button"
|
|
|
|
|
+ @click="searchProducts"
|
|
|
|
|
+ >
|
|
|
|
|
+ @lang('admin::app.catalog.categories.edit.search-products')
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="max-h-[400px] overflow-y-auto">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="product in searchResults"
|
|
|
|
|
+ :key="'search-' + product.id"
|
|
|
|
|
+ class="flex items-center justify-between border-b border-slate-200 py-2.5 last:border-b-0 dark:border-gray-800"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p class="text-sm font-medium text-gray-800 dark:text-white" v-text="product.name"></p>
|
|
|
|
|
+
|
|
|
|
|
+ <p class="text-xs text-gray-500 dark:text-gray-400" v-text="product.sku"></p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="primary-button !px-3 !py-1.5"
|
|
|
|
|
+ @click="attachProduct(product.id)"
|
|
|
|
|
+ >
|
|
|
|
|
+ @lang('admin::app.catalog.categories.edit.add-product')
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="searched && searchResults.length === 0"
|
|
|
|
|
+ class="py-6 text-center text-sm text-gray-500 dark:text-gray-400"
|
|
|
|
|
+ >
|
|
|
|
|
+ @lang('admin::app.catalog.categories.edit.no-products')
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </x-slot>
|
|
|
|
|
+ </x-admin::modal>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </script>
|
|
|
|
|
+
|
|
|
|
|
+ <script type="module">
|
|
|
|
|
+ app.component('v-category-products', {
|
|
|
|
|
+ template: '#v-category-products-template',
|
|
|
|
|
+
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ isLoading: true,
|
|
|
|
|
+
|
|
|
|
|
+ products: [],
|
|
|
|
|
+
|
|
|
|
|
+ childrenProducts: [],
|
|
|
|
|
+
|
|
|
|
|
+ searchQuery: '',
|
|
|
|
|
+
|
|
|
|
|
+ searchResults: [],
|
|
|
|
|
+
|
|
|
|
|
+ searched: false,
|
|
|
|
|
+ };
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ mounted() {
|
|
|
|
|
+ this.get();
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ get() {
|
|
|
|
|
+ this.isLoading = true;
|
|
|
|
|
+
|
|
|
|
|
+ axios.get("{{ route('admin.catalog.categories.products', $category->id) }}")
|
|
|
|
|
+ .then(response => {
|
|
|
|
|
+ this.isLoading = false;
|
|
|
|
|
+
|
|
|
|
|
+ this.products = response.data.products;
|
|
|
|
|
+
|
|
|
|
|
+ this.childrenProducts = response.data.children_products;
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(error => {
|
|
|
|
|
+ this.isLoading = false;
|
|
|
|
|
+
|
|
|
|
|
+ console.log(error);
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ openModal() {
|
|
|
|
|
+ this.searchQuery = '';
|
|
|
|
|
+
|
|
|
|
|
+ this.searchResults = [];
|
|
|
|
|
+
|
|
|
|
|
+ this.searched = false;
|
|
|
|
|
+
|
|
|
|
|
+ this.$refs.addProductModal.open();
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ searchProducts() {
|
|
|
|
|
+ if (! this.searchQuery.trim()) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.searched = true;
|
|
|
|
|
+
|
|
|
|
|
+ axios.get("{{ route('admin.catalog.products.search') }}", {
|
|
|
|
|
+ params: {
|
|
|
|
|
+ query: this.searchQuery,
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+ .then(response => {
|
|
|
|
|
+ this.searchResults = response.data.data ?? [];
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(error => {
|
|
|
|
|
+ console.log(error);
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ attachProduct(productId) {
|
|
|
|
|
+ axios.post("{{ route('admin.catalog.categories.products.attach', $category->id) }}", {
|
|
|
|
|
+ product_id: productId,
|
|
|
|
|
+ })
|
|
|
|
|
+ .then(response => {
|
|
|
|
|
+ this.$emitter.emit('add-flash', {
|
|
|
|
|
+ type: 'success',
|
|
|
|
|
+ message: response.data.message,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ this.$refs.addProductModal.close();
|
|
|
|
|
+
|
|
|
|
|
+ this.get();
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(error => {
|
|
|
|
|
+ console.log(error);
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ removeProduct(productId) {
|
|
|
|
|
+ axios.post("{{ route('admin.catalog.categories.products.detach', $category->id) }}", {
|
|
|
|
|
+ product_id: productId,
|
|
|
|
|
+ })
|
|
|
|
|
+ .then(response => {
|
|
|
|
|
+ this.$emitter.emit('add-flash', {
|
|
|
|
|
+ type: 'success',
|
|
|
|
|
+ message: response.data.message,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ this.get();
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(error => {
|
|
|
|
|
+ console.log(error);
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ onDragEnd() {
|
|
|
|
|
+ const payload = {
|
|
|
|
|
+ products: this.products.map((product, index) => ({
|
|
|
|
|
+ id: product.id,
|
|
|
|
|
+ position: this.products.length - index,
|
|
|
|
|
+ })),
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ this.savePositions(payload);
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ savePositions(payload) {
|
|
|
|
|
+ axios.post("{{ route('admin.catalog.categories.products.position', $category->id) }}", payload)
|
|
|
|
|
+ .then(response => {
|
|
|
|
|
+ this.$emitter.emit('add-flash', {
|
|
|
|
|
+ type: 'success',
|
|
|
|
|
+ message: response.data.message,
|
|
|
|
|
+ });
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(error => {
|
|
|
|
|
+ console.log(error);
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ updatePosition(product, event) {
|
|
|
|
|
+ const position = parseInt(event.target.value, 10);
|
|
|
|
|
+
|
|
|
|
|
+ if (Number.isNaN(position)) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ product.position = position;
|
|
|
|
|
+
|
|
|
|
|
+ axios.post("{{ route('admin.catalog.categories.products.position', $category->id) }}", {
|
|
|
|
|
+ product_id: product.id,
|
|
|
|
|
+ position: position,
|
|
|
|
|
+ })
|
|
|
|
|
+ .then(response => {
|
|
|
|
|
+ this.$emitter.emit('add-flash', {
|
|
|
|
|
+ type: 'success',
|
|
|
|
|
+ message: response.data.message,
|
|
|
|
|
+ });
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(error => {
|
|
|
|
|
+ console.log(error);
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ </script>
|
|
|
|
|
+@endPushOnce
|