Browse Source

see also #379

ali 2 năm trước cách đây
mục cha
commit
a642247127
53 tập tin đã thay đổi với 3282 bổ sung và 75 xóa
  1. 2 2
      package.json
  2. 4 2
      src/api/APIGateway.js
  3. 57 0
      src/api/models/StoreCategory.js
  4. 68 34
      src/components/FormBuilderCustumComponents/ContentCategorySelector.vue
  5. 11 8
      src/components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue
  6. 137 0
      src/components/FormBuilderCustumComponents/ProductSelector.vue
  7. 5 0
      src/components/Template/Header/MainHeader.vue
  8. 53 0
      src/components/Template/SideBar/AdminPanelSideBar.vue
  9. 0 0
      src/components/Template/SideBar/Main.vue
  10. 3 3
      src/components/Template/SideBar/TemplateSideBar.vue
  11. 0 0
      src/components/Template/SideBar/UserPanelSideBar.vue
  12. 2 2
      src/components/Widgets/ContentCategory/AdminContentCategoryShow/AdminContentCategoryShow.vue
  13. 21 0
      src/components/Widgets/Store/Shop/OptionPanel.vue
  14. 218 0
      src/components/Widgets/Store/Shop/Shop.vue
  15. 115 0
      src/components/Widgets/Store/StoreCategory/AdminStoreCategoryCreate/AdminStoreCategoryCreate.vue
  16. 21 0
      src/components/Widgets/Store/StoreCategory/AdminStoreCategoryCreate/OptionPanel.vue
  17. 148 0
      src/components/Widgets/Store/StoreCategory/AdminStoreCategoryList/AdminStoreCategoryList.vue
  18. 21 0
      src/components/Widgets/Store/StoreCategory/AdminStoreCategoryList/OptionPanel.vue
  19. 140 0
      src/components/Widgets/Store/StoreCategory/AdminStoreCategoryShow/AdminStoreCategoryShow.vue
  20. 21 0
      src/components/Widgets/Store/StoreCategory/AdminStoreCategoryShow/OptionPanel.vue
  21. 123 0
      src/components/Widgets/Store/StorePackage/AdminStorePackageCreate/AdminStorePackageCreate.vue
  22. 21 0
      src/components/Widgets/Store/StorePackage/AdminStorePackageCreate/OptionPanel.vue
  23. 179 0
      src/components/Widgets/Store/StorePackage/AdminStorePackageList/AdminStorePackageList.vue
  24. 21 0
      src/components/Widgets/Store/StorePackage/AdminStorePackageList/OptionPanel.vue
  25. 139 0
      src/components/Widgets/Store/StorePackage/AdminStorePackageShow/AdminStorePackageShow.vue
  26. 21 0
      src/components/Widgets/Store/StorePackage/AdminStorePackageShow/OptionPanel.vue
  27. 21 0
      src/components/Widgets/Store/StorePackage/StorePackageList/OptionPanel.vue
  28. 107 0
      src/components/Widgets/Store/StorePackage/StorePackageList/StorePackageList.vue
  29. 21 0
      src/components/Widgets/Store/StorePackage/StorePackageShow/OptionPanel.vue
  30. 277 0
      src/components/Widgets/Store/StorePackage/StorePackageShow/StorePackageShow.vue
  31. 112 0
      src/components/Widgets/Store/StoreProduct/AdminStoreProductCreate/AdminStoreProductCreate.vue
  32. 21 0
      src/components/Widgets/Store/StoreProduct/AdminStoreProductCreate/OptionPanel.vue
  33. 148 0
      src/components/Widgets/Store/StoreProduct/AdminStoreProductList/AdminStoreProductList.vue
  34. 21 0
      src/components/Widgets/Store/StoreProduct/AdminStoreProductList/OptionPanel.vue
  35. 141 0
      src/components/Widgets/Store/StoreProduct/AdminStoreProductShow/AdminStoreProductShow.vue
  36. 21 0
      src/components/Widgets/Store/StoreProduct/AdminStoreProductShow/OptionPanel.vue
  37. 21 0
      src/components/Widgets/Store/StoreProduct/StoreProductList/OptionPanel.vue
  38. 107 0
      src/components/Widgets/Store/StoreProduct/StoreProductList/StoreProductList.vue
  39. 21 0
      src/components/Widgets/Store/StoreProduct/StoreProductShow/OptionPanel.vue
  40. 277 0
      src/components/Widgets/Store/StoreProduct/StoreProductShow/StoreProductShow.vue
  41. 1 1
      src/layouts/MainLayout.vue
  42. 17 1
      src/models/Package.js
  43. 14 0
      src/models/Product.js
  44. 23 0
      src/models/StoreCategory.js
  45. 23 0
      src/pages/Admin/StoreCategory/Create.vue
  46. 23 0
      src/pages/Admin/StoreCategory/List.vue
  47. 23 0
      src/pages/Admin/StoreCategory/Show.vue
  48. 23 0
      src/pages/Admin/StoreProduct/Create.vue
  49. 23 0
      src/pages/Admin/StoreProduct/List.vue
  50. 23 0
      src/pages/Admin/StoreProduct/Show.vue
  51. 37 0
      src/router/routes/Admin.js
  52. 8 0
      src/router/routes/Public.js
  53. 177 22
      yarn.lock

+ 2 - 2
package.json

@@ -48,8 +48,8 @@
     "process": "^0.11.10",
     "quasar": "^2.14.2",
     "quasar-app-extension-q-page-builder": "^0.4.4",
-    "quasar-crud": "^1.0.9",
-    "quasar-form-builder": "^1.0.6",
+    "quasar-crud": "^2.1.0",
+    "quasar-form-builder": "^2.4.1",
     "quasar-template-builder": "^1.0.23",
     "quasar-ui-q-page-builder": "^0.4.4",
     "socket.io-client": "^4.6.1",

+ 4 - 2
src/api/APIGateway.js

@@ -8,7 +8,6 @@ import TestAPI from 'src/api/models/Test.js'
 import EventAPI from 'src/api/models/Event.js'
 import MediaAPI from 'src/api/models/Media.js'
 import NoticeAPI from 'src/api/models/Notice.js'
-import NoticeReceiverAPI from 'src/api/models/NoticeReceiver'
 import TicketAPI from 'src/api/models/Ticket.js'
 import WalletAPI from 'src/api/models/Wallet.js'
 import TestSetAPI from 'src/api/models/TestSet.js'
@@ -31,7 +30,9 @@ import PostCategoryAPI from 'src/api/models/PostCategory.js'
 import MediaHashtagAPI from 'src/api/models/MediaHashtag.js'
 import RegistrationAPI from 'src/api/models/Registration.js'
 import UnitCategoryAPI from 'src/api/models/UnitCategory.js'
+import StoreCategoryAPI from 'src/api/models/StoreCategory.js'
 import MediaCategoryAPI from 'src/api/models/MediaCategory.js'
+import NoticeReceiverAPI from 'src/api/models/NoticeReceiver.js'
 import QuestionChoiceApi from 'src/api/models/QuestionChoice.js'
 import TicketCategoryAPI from 'src/api/models/TicketCategory.js'
 import SessionTemplateAPI from 'src/api/models/SessionTemplate.js'
@@ -59,7 +60,6 @@ export const APIGateway = {
   event: new EventAPI(),
   media: new MediaAPI(),
   notice: new NoticeAPI(),
-  noticeReceiver: new NoticeReceiverAPI(),
   ticket: new TicketAPI(),
   wallet: new WalletAPI(),
   testSet: new TestSetAPI(),
@@ -82,7 +82,9 @@ export const APIGateway = {
   postCategory: new PostCategoryAPI(),
   unitCategory: new UnitCategoryAPI(),
   mediaHashtag: new MediaHashtagAPI(),
+  storeCategory: new StoreCategoryAPI(),
   mediaCategory: new MediaCategoryAPI(),
+  noticeReceiver: new NoticeReceiverAPI(),
   ticketCategory: new TicketCategoryAPI(),
   questionChoice: new QuestionChoiceApi(),
   transcriptSheet: new TranscriptSheetAPI(),

+ 57 - 0
src/api/models/StoreCategory.js

@@ -0,0 +1,57 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { StoreCategory, StoreCategoryList } from 'src/models/StoreCategory.js'
+
+export default class StoreCategoryAPI extends APIRepository {
+  constructor() {
+    super('store-category', appApi)
+    this.APIAdresses = {
+      base: '/store/categories',
+      byId: (id) => '/store/categories/' + id
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new StoreCategory(response.data) },
+      post: (response) => { return new StoreCategory(response.data) },
+      put: (response) => { return new StoreCategory(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        parent__isnull: null, // String (true, false)
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new StoreCategoryList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 68 - 34
src/components/FormBuilderCustumComponents/ContentCategorySelector.vue

@@ -1,34 +1,48 @@
 <template>
-  <div class="ContentCategorySelector row q-col-gutter-md">
-    <div class="col-md-4 col-12">
-      <div>
-        دسته بندی اصلی
+  <q-card>
+    <q-card-section>
+      <span v-if="categoryType === 'content'">
+        انتخاب دسته محتوا
+      </span>
+      <span v-else-if="categoryType === 'store'">
+        انتخاب دسته فروشگاه
+      </span>
+    </q-card-section>
+    <q-separator />
+    <q-card-section>
+      <div class="ContentCategorySelector row q-col-gutter-md">
+        <div class="col-md-4 col-12">
+          <div>
+            دسته بندی اصلی
+          </div>
+          <q-select v-model="mainCategory"
+                    :options="mainCategoryOptions"
+                    :loading="categories.laoding" />
+        </div>
+        <div class="col-md-4 col-12">
+          <div>
+            دسته بندی جزیی
+          </div>
+          <q-select v-model="subCategory"
+                    :options="subCategoryOptions"
+                    :loading="categories.laoding" />
+        </div>
+        <div class="col-md-4 col-12">
+          <div>
+            بخش
+          </div>
+          <q-select v-model="bakhshCategory"
+                    :options="bakhshCategoryOptions"
+                    :loading="categories.laoding" />
+        </div>
       </div>
-      <q-select v-model="mainCategory"
-                :options="mainCategoryOptions"
-                :loading="contentCategories.laoding" />
-    </div>
-    <div class="col-md-4 col-12">
-      <div>
-        دسته بندی جزیی
-      </div>
-      <q-select v-model="subCategory"
-                :options="subCategoryOptions"
-                :loading="contentCategories.laoding" />
-    </div>
-    <div class="col-md-4 col-12">
-      <div>
-        بخش
-      </div>
-      <q-select v-model="bakhshCategory"
-                :options="bakhshCategoryOptions"
-                :loading="contentCategories.laoding" />
-    </div>
-  </div>
+    </q-card-section>
+  </q-card>
 </template>
 
 <script>
 import { APIGateway } from 'src/api/APIGateway.js'
+import { StoreCategoryList } from 'src/models/StoreCategory.js'
 import { ContentCategoryList } from 'src/models/ContentCategory.js'
 
 export default {
@@ -41,6 +55,10 @@ export default {
     disable: {
       default: false,
       type: Boolean
+    },
+    categoryType: {
+      type: String,
+      default: 'content'
     }
   },
   emits: ['update:value'],
@@ -52,12 +70,12 @@ export default {
       mainCategoryOptions: [],
       subCategoryOptions: [],
       bakhshCategoryOptions: [],
-      contentCategories: new ContentCategoryList()
+      categories: this.categoryType === 'content' ? new ContentCategoryList() : new StoreCategoryList()
     }
   },
   watch: {
     value () {
-      if (this.value.id) {
+      if (this.value?.id) {
         this.getCategories()
       } else {
         this.inputData = this.value
@@ -66,6 +84,10 @@ export default {
     mainCategory (newValue) {
       this.subCategory = null
       this.subCategoryOptions = []
+      if (!newValue.item.children) {
+        this.$emit('update:value', newValue.value)
+        return
+      }
       this.subCategoryOptions = newValue.item.children.map(item => {
         return {
           item,
@@ -77,6 +99,10 @@ export default {
     subCategory (newValue) {
       this.bakhshCategory = null
       this.bakhshCategoryOptions = []
+      if (!newValue.item.children) {
+        this.$emit('update:value', newValue.value)
+        return
+      }
       this.bakhshCategoryOptions = newValue.item.children.map(item => {
         return {
           item,
@@ -113,12 +139,20 @@ export default {
       this.bakhshCategory = this.bakhshCategoryOptions.find(item => item.value === this.value.id)
     },
     getCategories () {
-      this.contentCategories.laoding = true
-      APIGateway.contentCategory.index({ parent__isnull: 'true', per_page: 99999 })
-        .then(contentCategories => {
-          this.contentCategories = new ContentCategoryList(contentCategories.list)
-          this.contentCategories.laoding = false
-          this.mainCategoryOptions = this.contentCategories.list.map(item => {
+      this.categories.laoding = true
+      const apiGateway = this.categoryType === 'content' ? APIGateway.contentCategory.index({ parent__isnull: 'true', per_page: 99999 })
+        : APIGateway.storeCategory.index({ parent__isnull: 'true', per_page: 99999 })
+
+      apiGateway
+        .then(categories => {
+          if (this.categoryType === 'content') {
+            this.categories = new ContentCategoryList(categories.list)
+          } else {
+            this.categories = new StoreCategoryList(categories.list)
+          }
+
+          this.categories.laoding = false
+          this.mainCategoryOptions = this.categories.list.map(item => {
             return {
               item,
               label: item.title,
@@ -140,7 +174,7 @@ export default {
           }
         })
         .catch(() => {
-          this.contentCategories.laoding = false
+          this.categories.laoding = false
         })
     }
   }

+ 11 - 8
src/components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue

@@ -14,13 +14,16 @@
                      :show-edit-button="false"
                      :show-expand-button="false"
                      :show-save-button="false"
-                     :show-reload-button="false" />
-      <q-btn color="primary"
-             class="full-width q-mt-md"
-             :loading="entityCreateLoading"
-             @click="entityCreate">
-        افزودن چند رسانه ای
-      </q-btn>
+                     :show-reload-button="false">
+        <template #after-form-builder>
+          <q-btn color="primary"
+                 class="full-width q-mt-md"
+                 :loading="entityCreateLoading"
+                 @click="entityCreate">
+            افزودن چند رسانه ای
+          </q-btn>
+        </template>
+      </entity-create>
     </div>
     <media-category v-if="imageMedias.list.length > 0"
                     :medias="imageMedias"
@@ -139,7 +142,7 @@ export default {
     value: {
       handler () {
         this.inputData = this.value
-        if (this.value.length > 0 && this.value[0].id) {
+        if (this.value && this.value.length > 0 && this.value[0].id) {
           this.medias = new MediaList(this.value)
           this.$emit('update:value', this.medias.list.map(item => item.id))
         }

+ 137 - 0
src/components/FormBuilderCustumComponents/ProductSelector.vue

@@ -0,0 +1,137 @@
+<template>
+  <entity-index v-model:table-selected-values="selected"
+                :value="inputs"
+                :title="label"
+                :api="api"
+                :table="table"
+                :table-keys="tableKeys"
+                :table-selection-mode="selectionMode"
+                :show-close-button="false"
+                :show-expand-button="false"
+                :show-reload-button="false"
+                :show-search-button="false"
+                :row-key="itemIdentifyKey"
+                :item-indicator-key="itemIndicatorKey"
+                @onInputClick="onInputClick"
+                @update:table-selected-values="onSelectedUpdate" />
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityIndex } from 'quasar-crud'
+import { Product } from 'src/models/Product.js'
+import { inputMixin } from 'quasar-form-builder'
+import { APIGateway } from 'src/api/APIGateway.js'
+import ContentCategorySelector from 'src/components/FormBuilderCustumComponents/ContentCategorySelector.vue'
+
+const ContentCategorySelectorComp = shallowRef(ContentCategorySelector)
+
+export default {
+  name: 'EntityInput',
+  components: {
+    EntityIndex
+  },
+  mixins: [inputMixin],
+  props: {
+    value: {
+      default: null
+    },
+    selectionMode: {
+      type: String,
+      default: 'single'
+    }
+  },
+  emits: ['onInputClick'],
+  data () {
+    return {
+      selected: [],
+      api: APIGateway.product.APIAdresses.base,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [
+        { type: ContentCategorySelectorComp, name: 'content_category', responseKey: 'content_category_info', categoryType: 'content', col: 'col-md-12 col-12' },
+        { type: 'select', name: 'is_physical', label: 'جنس محصول', options: (new Product()).is_physicalEnums, placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'select', name: 'physical_type', label: 'نوع محصول', options: (new Product()).physical_typeEnums, placeholder: ' ', col: 'col-md-6 col-12' }
+      ],
+      table: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شناسه',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'نام محصول',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'is_physical_info',
+            required: true,
+            label: 'جنس',
+            align: 'left',
+            field: row => (new Product(row)).is_physical_info.label
+          },
+          {
+            name: 'content_category',
+            required: true,
+            label: 'بخش',
+            align: 'left',
+            field: row => row.content_category
+          },
+          {
+            name: 'unit_price',
+            required: true,
+            label: 'قیمت',
+            align: 'left',
+            field: row => parseInt(row.unit_price.toString()).toLocaleString('fa')
+          },
+          {
+            name: 'is_visible',
+            required: true,
+            label: 'نمایش',
+            align: 'left',
+            field: '-'
+          }
+        ]
+      },
+      itemIdentifyKey: 'id',
+      itemIndicatorKey: (product) => product.title + '(' + product.id + ')'
+    }
+  },
+  methods: {
+    onInputClick(event) {
+      this.$emit('onInputClick', event)
+    },
+    onSelectedUpdate (values) {
+      let selected = (this.selectionMode === 'multiple') ? [] : null
+      values.forEach(vlue => {
+        if (this.selectionMode === 'multiple') {
+          selected.push(vlue)
+        } else {
+          selected = vlue
+        }
+      })
+      this.selected = selected
+      this.inputData = selected
+      if (this.selectionMode === 'multiple') {
+        this.$emit('update:value', selected.map(product => product.id))
+      } else {
+        this.$emit('update:value', selected.id)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+</style>

+ 5 - 0
src/components/Template/Header/MainHeader.vue

@@ -33,6 +33,11 @@
                    label="راهنما"
                    :to="{ name: 'Public.Post.List.Guide' }"
                    class="top-menu-item" />
+            <q-btn stretch
+                   flat
+                   label="Shop"
+                   :to="{ name: 'Public.Shop' }"
+                   class="top-menu-item" />
             <q-btn stretch
                    flat
                    label="فروشگاه"

+ 53 - 0
src/components/Template/SideBard/AdminPanelSideBar.vue → src/components/Template/SideBar/AdminPanelSideBar.vue

@@ -219,6 +219,59 @@ export default {
             }
           ]
         },
+        {
+          title: 'فروشگاه',
+          roles: ['superuser'],
+          icon: 'storefront',
+          routeName: null,
+          show: true,
+          open: true,
+          active: false,
+          children: [
+            {
+              title: 'دسته بندی ها',
+              roles: ['superuser'],
+              routeName: 'Admin.Store.Category.List',
+              show: true,
+              active: false
+            },
+            {
+              title: 'محصولات',
+              roles: ['superuser'],
+              routeName: 'Admin.Store.Package.List',
+              show: true,
+              active: false
+            },
+            {
+              title: 'سفارشات',
+              roles: ['superuser'],
+              routeName: 'Admin.Store.Package.List',
+              show: true,
+              active: false
+            },
+            {
+              title: 'کد تخفیف',
+              roles: ['superuser'],
+              routeName: 'Admin.Store.Package.List',
+              show: true,
+              active: false
+            },
+            {
+              title: 'تنظیمات',
+              roles: ['superuser'],
+              routeName: 'Admin.Store.Package.List',
+              show: true,
+              active: false
+            },
+            {
+              title: 'درخواست ها',
+              roles: ['superuser'],
+              routeName: 'Admin.Store.Package.List',
+              show: true,
+              active: false
+            }
+          ]
+        },
         {
           title: 'اعلان‌ها',
           roles: ['superuser'],

+ 0 - 0
src/components/Template/SideBard/Main.vue → src/components/Template/SideBar/Main.vue


+ 3 - 3
src/components/Template/SideBard/TemplateSideBar.vue → src/components/Template/SideBar/TemplateSideBar.vue

@@ -5,9 +5,9 @@
 </template>
 
 <script>
-import MainSideBarTemplate from 'src/components/Template/SideBard/Main.vue'
-import UserPanelSideBar from 'src/components/Template/SideBard/UserPanelSideBar.vue'
-import AdminPanelSideBar from 'src/components/Template/SideBard/AdminPanelSideBar.vue'
+import MainSideBarTemplate from 'src/components/Template/SideBar/Main.vue'
+import UserPanelSideBar from 'src/components/Template/SideBar/UserPanelSideBar.vue'
+import AdminPanelSideBar from 'src/components/Template/SideBar/AdminPanelSideBar.vue'
 
 export default {
   name: 'TemplateSideBar',

+ 0 - 0
src/components/Template/SideBard/UserPanelSideBar.vue → src/components/Template/SideBar/UserPanelSideBar.vue


+ 2 - 2
src/components/Widgets/ContentCategory/AdminContentCategoryShow/AdminContentCategoryShow.vue

@@ -60,7 +60,7 @@
 import { shallowRef } from 'vue'
 import { mixinWidget } from 'src/mixin/Mixins.js'
 import { APIGateway } from 'src/api/APIGateway.js'
-import { EntityEdit, EntityCreate } from 'quasar-crud'
+import { EntityEdit } from 'quasar-crud'
 import { FormBuilderAssist } from 'quasar-form-builder'
 import BtnControl from 'src/components/Control/btn.vue'
 import { ContentCategory } from 'src/models/ContentCategory.js'
@@ -76,7 +76,7 @@ export default {
     AdminContentCategoryCreate,
     EntityEdit
   },
-  mixins: [mixinWidget, EntityCreate],
+  mixins: [mixinWidget],
   data: () => {
     return {
       listKey: Date.now(),

+ 21 - 0
src/components/Widgets/Store/Shop/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 218 - 0
src/components/Widgets/Store/Shop/Shop.vue

@@ -0,0 +1,218 @@
+<template>
+  <div class="shop-widget"
+       :style="localOptions.style">
+    <q-banner>
+      محصولات
+    </q-banner>
+    <div class="shop-widget__content">
+      <div class="shop-widget__sidebar">
+        shop-widget__sidebar
+      </div>
+      <div class="shop-widget__main">
+        <div class="shop-widget__main-top-filter">
+          <div class="shop-widget__main-top-filter-tools">
+            <div class="shop-widget__main-top-filter-txt">
+              <q-input v-model="filters.search"
+                       label="جستجوی محصولات">
+                <template #prepend>
+                  <q-icon name="search" />
+                </template>
+              </q-input>
+            </div>
+            <div class="shop-widget__main-top-filter-sort">
+              <div class="shop-widget__main-top-filter-sort-label">
+                ترتیب نمایش
+              </div>
+              <div class="shop-widget__main-top-filter-sort-select">
+                <q-select v-model="filters.sort"
+                          emit-value
+                          map-options
+                          :options="sortOptions" />
+              </div>
+            </div>
+          </div>
+          <div class="shop-widget__main-top-filter-tabs">
+            <q-tabs v-model="searchTab"
+                    align="left"
+                    active-bg-color="primary"
+                    active-color="white">
+              <q-tab label="محصولات"
+                     name="product" />
+              <q-tab label="بسته"
+                     name="package" />
+            </q-tabs>
+          </div>
+        </div>
+        <div class="shop-widget__result">
+          shop-widget__result
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+// import { EntityIndex } from 'quasar-crud'
+import { Ticket } from 'src/models/Ticket.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'ContentList',
+  // components: { EntityIndex },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      searchTab: 'product',
+      filters: {
+        search: null,
+        sort: 'sort1'
+      },
+      sortOptions: [
+        {
+          label: 'پرفروش ترین',
+          value: 'sort1'
+        },
+        {
+          label: 'گران ترین',
+          value: 'sort2'
+        },
+        {
+          label: 'ارزان ترین',
+          value: 'sort3'
+        }
+      ],
+      api: APIGateway.ticket.APIAdresses.base,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [],
+      table: {
+        columns: [
+          {
+            name: 'title',
+            required: true,
+            label: 'موضوع پیام',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'sender',
+            required: true,
+            label: 'فرستنده',
+            align: 'left',
+            field: row => row.creator_info.firstname + ' ' + row.creator_info.lastname
+          },
+          {
+            name: 'status',
+            required: true,
+            label: 'وضعیت',
+            align: 'left',
+            field: row => (new Ticket(row)).status_info.label
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'جزییات',
+            align: 'left',
+            field: row => ''
+          }
+        ]
+      },
+      mounted: false,
+      createRouteName: ''
+    }
+  },
+  mounted() {
+    this.mounted = true
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.shop-widget {
+  .shop-widget__content {
+    display: flex;
+    gap: 24px;
+    justify-content: flex-start;
+    $sidebar-width: 312px;
+    .shop-widget__sidebar {
+      width: $sidebar-width;
+      background: #F6F6F6;
+      border-radius: 20px;
+      padding: 24px;
+    }
+    .shop-widget__main {
+      width: calc( 100% - #{$sidebar-width} );
+      .shop-widget__main-top-filter {
+        .shop-widget__main-top-filter-tools {
+          display: flex;
+          justify-content: flex-start;
+          gap: 24px;
+          $sort-width: 312px;
+          margin-bottom: 20px;
+          .shop-widget__main-top-filter-sort {
+            width: $sort-width;
+            gap: 24px;
+            display: flex;
+            align-items: center;
+            justify-content: flex-start;
+            .shop-widget__main-top-filter-sort-label {}
+            .shop-widget__main-top-filter-sort-select {
+              flex: 1;
+            }
+          }
+          .shop-widget__main-top-filter-txt {
+            width: calc( 100% - #{$sort-width} );
+            :deep(.q-field) {
+              .q-field__inner {
+                .q-field__control {
+                  .q-field__prepend {
+                    padding: 0;
+                    width: 36px;
+                    justify-content: flex-end;
+                    height: 100%;
+                    font-size: 26px;
+                  }
+                  .q-field__control-container {
+                    .q-field__native {
+                      padding-left: 4px;
+                    }
+                    .q-field__label {
+                      margin-left: 4px;
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+        .shop-widget__main-top-filter-tabs {
+          padding: 12px 20px;
+          border: 1px solid #DFE1EC;
+          border-radius: 8px 8px 0 0;
+          :deep(.q-tabs) {
+            .q-tabs__content {
+              .q-tab {
+                border-radius: 8px;
+                min-height: 45px;
+                width: 160px;
+                .q-tab__indicator {
+                  display: none;
+                }
+              }
+            }
+          }
+        }
+      }
+      .shop-widget__result {
+
+      }
+    }
+  }
+}
+</style>

+ 115 - 0
src/components/Widgets/Store/StoreCategory/AdminStoreCategoryCreate/AdminStoreCategoryCreate.vue

@@ -0,0 +1,115 @@
+<template>
+  <div class="AdminStoreCategoryCreate"
+       :style="localOptions.style">
+    <div v-if="showBackBtn"
+         class="flex justify-end">
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-create v-if="mounted"
+                   ref="entityCreate"
+                   v-model:value="inputs"
+                   :title="title"
+                   :api="api"
+                   :entity-id-key="entityIdKey"
+                   :entity-param-key="entityParamKey"
+                   :show-route-name="showRouteName"
+                   :show-close-button="false"
+                   :show-edit-button="false"
+                   :show-expand-button="false"
+                   :show-save-button="false"
+                   :show-reload-button="false" />
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityCreate } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import BtnControl from 'components/Control/btn.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+
+export default {
+  name: 'AdminStoreCategoryCreate',
+  components: { EntityCreate },
+  mixins: [mixinWidget],
+  props: {
+    title: {
+      type: String,
+      default: 'ایجاد دسته محصول'
+    },
+    parent: {
+      type: Number,
+      default: null
+    },
+    showBackBtn: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data () {
+    return {
+      mounted: false,
+      entityLoading: false,
+      api: APIGateway.storeCategory.APIAdresses.base,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Store.Category.Show',
+      indexRouteName: 'Admin.Store.Category.List',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'file', name: 'thumbnail', responseKey: 'thumbnail', label: 'عکس', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', placeholder: ' ', col: 'col-md-12 col-12' },
+        { type: 'hidden', name: 'parent', responseKey: 'parent', value: this.parent },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'تایید', placeholder: ' ', atClick: this.create, col: 'col-12' }
+      ]
+    }
+  },
+  created() {
+    FormBuilderAssist.setAttributeByName(this.inputs, 'parent', 'value', this.parent)
+  },
+  mounted() {
+    this.mounted = true
+  },
+  methods: {
+    create() {
+      const redirect = !this.parent
+      this.entityLoading = true
+      this.$refs.entityCreate.createEntity(redirect)
+        .then(() => {
+          this.entityLoading = false
+          this.$emit('onCreated')
+        })
+        .catch(() => {
+          this.entityLoading = false
+        })
+    },
+    setInputOptions (name, options) {
+      FormBuilderAssist.setAttributeByName(this.inputs, name, 'options', options)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminStoreCategoryCreate {
+  :deep(.q-card.entity-create) {
+    .q-card__section {
+      .q-expansion-item {
+        .q-expansion-item__container {
+          .q-item {
+            display: none;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Store/StoreCategory/AdminStoreCategoryCreate/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 148 - 0
src/components/Widgets/Store/StoreCategory/AdminStoreCategoryList/AdminStoreCategoryList.vue

@@ -0,0 +1,148 @@
+<template>
+  <div class="AdminStoreCategoryList"
+       :style="localOptions.style">
+    <div v-if="!parent"
+         class="more-action">
+      <q-btn label="دسته محصول جدید"
+             color="primary"
+             :to="{name: 'Admin.Store.Category.Create'}" />
+    </div>
+    <entity-index v-if="mounted"
+                  v-model:value="inputs"
+                  :title="title"
+                  :api="api"
+                  :table="table"
+                  :table-keys="tableKeys"
+                  :create-route-name="createRouteName"
+                  :show-search-button="false"
+                  :show-expand-button="false"
+                  :show-reload-button="false">
+      <template #entity-index-table-cell="{inputData}">
+        <template v-if="inputData.col.name === 'number'">
+          {{ inputData.rowNumber }}
+        </template>
+        <template v-else-if="inputData.col.name === 'thumbnail'">
+          <q-img :src="inputData.col.value"
+                 width="100px" />
+        </template>
+        <template v-else-if="inputData.col.name === 'action'">
+          <q-btn color="primary"
+                 :to="{name: 'Admin.Store.Category.Show', params: {id: inputData.props.row.id}}">
+            مشاهده جزییات
+          </q-btn>
+        </template>
+        <template v-else>
+          {{ inputData.col.value }}
+        </template>
+      </template>
+    </entity-index>
+  </div>
+</template>
+
+<script>
+import Assist from 'assets/js/Assist.js'
+import { EntityIndex } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+
+export default {
+  name: 'AdminStoreCategoryList',
+  components: { EntityIndex },
+  mixins: [mixinWidget],
+  props: {
+    title: {
+      type: String,
+      default: 'لیست دسته های محصول'
+    },
+    parent: {
+      type: Number,
+      default: null
+    }
+  },
+  data: () => {
+    return {
+      api: APIGateway.storeCategory.APIAdresses.base,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [
+        { type: 'hidden', name: 'parent', value: null },
+        { type: 'hidden', name: 'parent__isnull', value: null }
+      ],
+      table: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'thumbnail',
+            required: true,
+            label: 'تصویر',
+            align: 'left',
+            field: row => row.thumbnail
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'نام دسته',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'creation_time',
+            required: true,
+            label: 'تاریخ ایجاد',
+            align: 'left',
+            field: row => Assist.miladiToShamsi(row.creation_time)
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'جزییات',
+            align: 'left',
+            field: row => ''
+          }
+        ]
+      },
+      mounted: false,
+      createRouteName: ''
+    }
+  },
+  created() {
+    FormBuilderAssist.setAttributeByName(this.inputs, 'parent', 'value', this.parent)
+    if (!this.parent) {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'parent__isnull', 'value', 'true')
+    }
+  },
+  mounted() {
+    this.mounted = true
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminStoreCategoryList {
+  :deep(.entity-index) {
+    .q-card__section {
+      .q-item {
+        display: none;
+      }
+    }
+  }
+  .more-action {
+    display: flex;
+    flex-flow: row;
+    justify-content: flex-end;
+    margin-bottom: 10px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Store/StoreCategory/AdminStoreCategoryList/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 140 - 0
src/components/Widgets/Store/StoreCategory/AdminStoreCategoryShow/AdminStoreCategoryShow.vue

@@ -0,0 +1,140 @@
+<template>
+  <div class="AdminStoreCategoryShow"
+       :style="localOptions.style">
+    <div class="flex justify-end">
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-edit v-if="mounted"
+                 ref="entityEdit"
+                 v-model:value="inputs"
+                 title="مشخصات دسته بندی کلی"
+                 :api="api"
+                 :entity-id-key="entityIdKey"
+                 :entity-param-key="entityParamKey"
+                 :show-route-name="showRouteName"
+                 :show-close-button="false"
+                 :show-edit-button="false"
+                 :show-expand-button="false"
+                 :show-save-button="false"
+                 :show-reload-button="false"
+                 :redirect-after-edit="false"
+                 :after-load-input-data="afterLoadInputData">
+      <template #after-form-builder>
+        <div v-if="storeCategory.id"
+             class="q-mt-lg">
+          <q-expansion-item expand-separator
+                            label="لیست دسته بندی ها">
+            <q-card>
+              <q-card-section>
+                <admin-store-category-list :key="listKey"
+                                           title="لیست دسته بندی های جزیی"
+                                           :parent="storeCategory.id" />
+              </q-card-section>
+            </q-card>
+          </q-expansion-item>
+          <q-expansion-item expand-separator
+                            label="ایجاد دسته بندی">
+            <q-card>
+              <q-card-section>
+                <admin-store-category-create :key="listKey"
+                                             title="ایجاد دسته بندی جزیی"
+                                             :parent="storeCategory.id"
+                                             :show-back-btn="false"
+                                             @onCreated="onCreated" />
+              </q-card-section>
+            </q-card>
+          </q-expansion-item>
+        </div>
+      </template>
+    </entity-edit>
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityEdit } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import BtnControl from 'components/Control/btn.vue'
+import { StoreCategory } from 'src/models/StoreCategory.js'
+import AdminStoreCategoryList from 'components/Widgets/Store/StoreCategory/AdminStoreCategoryList/AdminStoreCategoryList.vue'
+import AdminStoreCategoryCreate from 'components/Widgets/Store/StoreCategory/AdminStoreCategoryCreate/AdminStoreCategoryCreate.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+
+export default {
+  name: 'AdminStoreCategoryShow',
+  components: {
+    EntityEdit,
+    AdminStoreCategoryList,
+    AdminStoreCategoryCreate
+  },
+  mixins: [mixinWidget],
+  data () {
+    const storeCategoryId = this.$route.params.id
+
+    return {
+      listKey: Date.now(),
+      replyText: null,
+      mounted: false,
+      entityLoading: true,
+      storeCategory: new StoreCategory(),
+      api: APIGateway.storeCategory.APIAdresses.byId(storeCategoryId),
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Ticket.Show',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', value: null, placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'file', name: 'thumbnail', responseKey: 'thumbnail', label: 'عکس', value: null, placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', value: null, placeholder: ' ', col: 'col-md-12 col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'تایید', placeholder: ' ', ignoreValue: false, atClick: this.edit, col: 'col-12' },
+        { type: 'hidden', name: 'parent', responseKey: 'parent', value: null },
+        { type: 'hidden', name: 'id', responseKey: 'id', value: null }
+      ]
+    }
+  },
+  computed: {
+    repliesInfo () {
+      return this.inputs.find(input => input.name === 'replies_info').value
+    }
+  },
+  created() {
+    this.storeCategory.loading = true
+  },
+  mounted() {
+    this.mounted = true
+  },
+  methods: {
+    afterLoadInputData (data) {
+      this.storeCategory = new StoreCategory(data)
+      this.storeCategory.loading = false
+      this.entityLoading = false
+    },
+    edit() {
+      this.entityLoading = true
+      this.$refs.entityEdit.editEntity(false)
+        .then(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+    },
+    onCreated () {
+      this.listKey = Date.now()
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminStoreCategoryShow {
+}
+</style>

+ 21 - 0
src/components/Widgets/Store/StoreCategory/AdminStoreCategoryShow/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 123 - 0
src/components/Widgets/Store/StorePackage/AdminStorePackageCreate/AdminStorePackageCreate.vue

@@ -0,0 +1,123 @@
+<template>
+  <div class="AdminContentCreate"
+       :style="localOptions.style">
+    <div class="flex justify-end">
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-create v-if="mounted"
+                   ref="entityCreate"
+                   v-model:value="inputs"
+                   title="ایجاد محتوا"
+                   :api="api"
+                   :entity-id-key="entityIdKey"
+                   :entity-param-key="entityParamKey"
+                   :show-route-name="showRouteName"
+                   :show-close-button="false"
+                   :show-edit-button="false"
+                   :show-expand-button="false"
+                   :show-save-button="false"
+                   :show-reload-button="false" />
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityCreate } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import BtnControl from 'components/Control/btn.vue'
+import ProductSelector from 'components/FormBuilderCustumComponents/ProductSelector.vue'
+import ContentMedias from 'components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue'
+import ContentCategorySelector from 'components/FormBuilderCustumComponents/ContentCategorySelector.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const ContentMediasComp = shallowRef(ContentMedias)
+const ProductSelectorComp = shallowRef(ProductSelector)
+const ContentCategorySelectorComp = shallowRef(ContentCategorySelector)
+
+export default {
+  name: 'AdminContentCreate',
+  components: { EntityCreate },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      mounted: false,
+      entityLoading: false,
+      api: APIGateway.content.APIAdresses.base,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Content.Show',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'file', name: 'thumbnail', responseKey: 'thumbnail', label: 'عکس', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: ContentCategorySelectorComp, name: 'content_category', responseKey: 'content_category_info', categoryType: 'content', col: 'col-md-12 col-12' },
+        { type: ContentCategorySelectorComp, name: 'store_category', responseKey: 'store_category_info', categoryType: 'store', col: 'col-md-12 col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', col: 'col-md-12 col-12' },
+        { type: 'inputEditor', name: 'specification', responseKey: 'specification', label: 'specification', col: 'col-md-12 col-12' },
+        { type: ContentMediasComp, name: 'medias', responseKey: 'medias_info', col: 'col-md-12 col-12' },
+        { type: 'input', name: 'unit_price', responseKey: 'unit_price', label: 'قیمت واحد', placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'input', name: 'discount_percent', responseKey: 'discount_percent', label: 'درصد تخفیف', placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'input', name: 'inventory', responseKey: 'inventory', label: 'موجودی', placeholder: ' ', col: 'col-md-2 col-12' },
+        { type: 'input', name: 'maximum_order_count', responseKey: 'maximum_order_count', label: 'سقف سفارش مجاز', placeholder: ' ', col: 'col-md-2 col-12' },
+        { type: 'input', name: 'weight', responseKey: 'weight', label: 'وزن', placeholder: ' ', col: 'col-md-2 col-12' },
+        { type: ProductSelectorComp, name: 'products', responseKey: 'products_info', label: 'انتخاب محصولات', selectionMode: 'multiple', col: 'col-12' },
+
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ساخت بسته', placeholder: ' ', ignoreValue: true, atClick: () => {}, col: 'col-md-6' }
+      ]
+    }
+  },
+  mounted() {
+    this.setActionBtn()
+    this.mounted = true
+  },
+  methods: {
+    setActionBtn () {
+      this.inputs.forEach((item, index) => {
+        if (item.name === 'btn') {
+          this.inputs[index].atClick = this.onSubmit
+        }
+      })
+    },
+    onSubmit () {
+      this.create()
+    },
+    create() {
+      this.entityLoading = true
+      this.$refs.entityCreate.createEntity()
+        .then(() => {
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminContentCreate {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    position: relative;
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Store/StorePackage/AdminStorePackageCreate/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 179 - 0
src/components/Widgets/Store/StorePackage/AdminStorePackageList/AdminStorePackageList.vue

@@ -0,0 +1,179 @@
+<template>
+  <div class="AdminStorePackageList"
+       :style="localOptions.style">
+    <div class="more-action">
+      <q-tab-panels v-model="tab">
+        <q-tab-panel name="products"
+                     class="q-pa-none">
+          <q-btn label="ایجاد محصول جدید"
+                 color="primary"
+                 :to="{name: 'Admin.Store.Package.Create'}" />
+        </q-tab-panel>
+        <q-tab-panel name="packages"
+                     class="q-pa-none">
+          <q-btn label="ایجاد بسته جدید"
+                 color="primary"
+                 :to="{name: 'Admin.Store.Package.Create'}" />
+        </q-tab-panel>
+      </q-tab-panels>
+    </div>
+    <q-card>
+      <div class="q-pa-sm">
+        <q-tabs v-model="tab"
+                align="left"
+                active-bg-color="primary"
+                active-color="white">
+          <q-tab name="products"
+                 label="محصولات" />
+          <q-tab name="packages"
+                 label="بسته ها" />
+        </q-tabs>
+      </div>
+      <q-separator />
+      <div>
+        <q-tab-panels v-model="tab">
+          <q-tab-panel name="products"
+                       class="q-pa-none">
+            <admin-store-product-list :show-back-btn="false" />
+          </q-tab-panel>
+          <q-tab-panel name="packages"
+                       class="q-pa-none">
+            <entity-index v-if="mounted"
+                          v-model:value="inputs"
+                          title="لیست بسته ها"
+                          :api="api"
+                          :table="table"
+                          :table-keys="tableKeys"
+                          :create-route-name="createRouteName"
+                          :show-search-button="false"
+                          :show-expand-button="false"
+                          :show-reload-button="false">
+              <template #entity-index-table-cell="{inputData}">
+                <template v-if="inputData.col.name === 'number'">
+                  {{ inputData.rowNumber }}
+                </template>
+                <template v-else-if="inputData.col.name === 'thumbnail'">
+                  <q-img :src="inputData.col.value"
+                         width="100px" />
+                </template>
+                <template v-else-if="inputData.col.name === 'action'">
+                  <q-btn color="primary"
+                         :to="{name: 'Admin.Store.Package.Show', params: {id: inputData.props.row.id}}">
+                    مشاهده جزییات
+                  </q-btn>
+                </template>
+                <template v-else>
+                  {{ inputData.col.value }}
+                </template>
+              </template>
+            </entity-index>
+          </q-tab-panel>
+        </q-tab-panels>
+      </div>
+    </q-card>
+  </div>
+</template>
+
+<script>
+import { EntityIndex } from 'quasar-crud'
+import { Package } from 'src/models/Package.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import AdminStoreProductList from 'components/Widgets/Store/StoreProduct/AdminStoreProductList/AdminStoreProductList.vue'
+
+export default {
+  name: 'AdminStorePackageList',
+  components: { EntityIndex, AdminStoreProductList },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      tab: 'packages',
+      api: APIGateway.package.APIAdresses.base,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [],
+      table: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'id',
+            required: true,
+            label: 'شناسه',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'نام محصول',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'جنس',
+            align: 'left',
+            field: row => (new Package(row)).is_physical_info.label
+          },
+          {
+            name: 'category_info',
+            required: true,
+            label: 'بخش',
+            align: 'left',
+            field: row => row.content_category
+          },
+          {
+            name: 'category_info',
+            required: true,
+            label: 'قیمت',
+            align: 'left',
+            field: row => parseInt(row.unit_price.toString()).toLocaleString('fa')
+          },
+          {
+            name: 'category_info',
+            required: true,
+            label: 'نمایش',
+            align: 'left',
+            field: '-'
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'جزییات',
+            align: 'left',
+            field: row => ''
+          }
+        ]
+      },
+      mounted: false,
+      createRouteName: ''
+    }
+  },
+  mounted() {
+    this.mounted = true
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminStorePackageList {
+  .more-action {
+    display: flex;
+    flex-flow: row;
+    justify-content: flex-end;
+    margin-bottom: 10px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Store/StorePackage/AdminStorePackageList/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 139 - 0
src/components/Widgets/Store/StorePackage/AdminStorePackageShow/AdminStorePackageShow.vue

@@ -0,0 +1,139 @@
+<template>
+  <div class="AdminStorePackageShow"
+       :style="localOptions.style">
+    <div class="flex justify-end">
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-edit v-if="mounted"
+                 ref="entityEdit"
+                 v-model:value="inputs"
+                 title="اطلاعات بسته"
+                 :api="api"
+                 :entity-id-key="entityIdKey"
+                 :entity-param-key="entityParamKey"
+                 :show-route-name="showRouteName"
+                 :show-close-button="false"
+                 :show-edit-button="false"
+                 :show-expand-button="false"
+                 :show-save-button="false"
+                 :show-reload-button="false"
+                 :redirect-after-edit="false"
+                 :after-load-input-data="afterLoadInputData" />
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityEdit } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import BtnControl from 'components/Control/btn.vue'
+import ProductSelector from 'components/FormBuilderCustumComponents/ProductSelector.vue'
+import ContentMedias from 'components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue'
+import ContentCategorySelector from 'components/FormBuilderCustumComponents/ContentCategorySelector.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const ContentMediasComp = shallowRef(ContentMedias)
+const ProductSelectorComp = shallowRef(ProductSelector)
+const ContentCategorySelectorComp = shallowRef(ContentCategorySelector)
+
+export default {
+  name: 'AdminStorePackageShow',
+  components: {
+    EntityEdit
+  },
+  mixins: [mixinWidget],
+  data () {
+    const packageId = this.$route.params.id
+    return {
+      mounted: false,
+      entityLoading: true,
+      api: APIGateway.package.APIAdresses.byId(packageId),
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Store.Package.Show',
+      indexRouteName: 'Admin.Store.Package.List',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'file', name: 'thumbnail', responseKey: 'thumbnail', label: 'عکس', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: ContentCategorySelectorComp, name: 'content_category', responseKey: 'content_category_info', categoryType: 'content', col: 'col-md-12 col-12' },
+        { type: ContentCategorySelectorComp, name: 'store_category', responseKey: 'store_category_info', categoryType: 'store', col: 'col-md-12 col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', col: 'col-md-12 col-12' },
+        { type: 'inputEditor', name: 'specification', responseKey: 'specification', label: 'specification', col: 'col-md-12 col-12' },
+        { type: ContentMediasComp, name: 'medias', responseKey: 'medias_info', col: 'col-md-12 col-12' },
+        { type: 'input', name: 'unit_price', responseKey: 'unit_price', label: 'قیمت واحد', placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'input', name: 'discount_percent', responseKey: 'discount_percent', label: 'درصد تخفیف', placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'input', name: 'inventory', responseKey: 'inventory', label: 'موجودی', placeholder: ' ', col: 'col-md-2 col-12' },
+        { type: 'input', name: 'maximum_order_count', responseKey: 'maximum_order_count', label: 'سقف سفارش مجاز', placeholder: ' ', col: 'col-md-2 col-12' },
+        { type: 'input', name: 'weight', responseKey: 'weight', label: 'وزن', placeholder: ' ', col: 'col-md-2 col-12' },
+        { type: ProductSelectorComp, name: 'products', responseKey: 'products_info', label: 'انتخاب محصولات', selectionMode: 'multiple', col: 'col-12' },
+
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ویرایش بسته', placeholder: ' ', ignoreValue: true, atClick: () => {}, col: 'col-md-6' }
+      ]
+    }
+  },
+  mounted() {
+    this.setActionBtn()
+    this.mounted = true
+  },
+  methods: {
+    setActionBtn () {
+      this.inputs.forEach((item, index) => {
+        if (item.name === 'btn') {
+          this.inputs[index].atClick = this.edit
+        }
+      })
+    },
+    afterLoadInputData () {
+      this.entityLoading = false
+    },
+    edit() {
+      this.entityLoading = true
+      this.$refs.entityEdit.editEntity(false)
+        .then(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminStorePackageShow {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    display: flex;
+    flex-flow: row;
+    position: relative;
+    .static-title {
+
+    }
+    .dynamic-title {
+
+    }
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Store/StorePackage/AdminStorePackageShow/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 21 - 0
src/components/Widgets/Store/StorePackage/StorePackageList/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 107 - 0
src/components/Widgets/Store/StorePackage/StorePackageList/StorePackageList.vue

@@ -0,0 +1,107 @@
+<template>
+  <div class="ContentList"
+       :style="localOptions.style">
+    <div class="more-action">
+      <q-btn label="ایجاد درخواست"
+             color="primary"
+             :to="{name: 'UserPanel.Ticket.Create'}" />
+    </div>
+    <entity-index v-if="mounted"
+                  ref="entityIndex"
+                  v-model:value="inputs"
+                  title="لیست درخواست ها"
+                  :api="api"
+                  :table="table"
+                  :table-keys="tableKeys"
+                  :create-route-name="createRouteName"
+                  :show-search-button="false"
+                  :show-expand-button="false"
+                  :show-reload-button="false">
+      <template #entity-index-table-cell="{inputData}">
+        <template v-if="inputData.col.name === 'action'">
+          <q-btn color="primary"
+                 :to="{name: 'UserPanel.Ticket.Show', params: {id: inputData.props.row.id}}">
+            مشاهده جزییات
+          </q-btn>
+        </template>
+        <template v-else>
+          {{ inputData.col.value }}
+        </template>
+      </template>
+    </entity-index>
+  </div>
+</template>
+
+<script>
+import { EntityIndex } from 'quasar-crud'
+import { Ticket } from 'src/models/Ticket.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'ContentList',
+  components: { EntityIndex },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      api: APIGateway.ticket.APIAdresses.base,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [],
+      table: {
+        columns: [
+          {
+            name: 'title',
+            required: true,
+            label: 'موضوع پیام',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'sender',
+            required: true,
+            label: 'فرستنده',
+            align: 'left',
+            field: row => row.creator_info.firstname + ' ' + row.creator_info.lastname
+          },
+          {
+            name: 'status',
+            required: true,
+            label: 'وضعیت',
+            align: 'left',
+            field: row => (new Ticket(row)).status_info.label
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'جزییات',
+            align: 'left',
+            field: row => ''
+          }
+        ]
+      },
+      mounted: false,
+      createRouteName: ''
+    }
+  },
+  mounted() {
+    this.mounted = true
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.ContentList {
+  .more-action {
+    display: flex;
+    flex-flow: row;
+    justify-content: flex-end;
+    margin-bottom: 10px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Store/StorePackage/StorePackageShow/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 277 - 0
src/components/Widgets/Store/StorePackage/StorePackageShow/StorePackageShow.vue

@@ -0,0 +1,277 @@
+<template>
+  <div class="ContentShow"
+       :style="localOptions.style">
+    <div class="title">
+      <div class="static-title">
+        مشاهده پیام
+      </div>
+      <div class="dynamic-title">
+        <template v-if="entityLoading">
+          <q-skeleton type="text"
+                      class="q-ml-lg"
+                      width="100px" />
+        </template>
+        <template v-else>
+          ({{ ticketTitle }})
+        </template>
+      </div>
+      <div class="back-action">
+        <q-btn flat
+               :to="{name: 'UserPanel.Ticket.List'}"
+               color="grey">
+          بازگشت
+          >
+        </q-btn>
+      </div>
+    </div>
+    <q-card class="form"
+            flat>
+      <entity-show v-if="mounted"
+                   ref="entityShow"
+                   v-model:value="inputs"
+                   title="اطلاعات کاربری"
+                   :api="api"
+                   :entity-id-key="entityIdKey"
+                   :entity-param-key="entityParamKey"
+                   :show-route-name="showRouteName"
+                   :show-close-button="false"
+                   :show-edit-button="false"
+                   :show-expand-button="false"
+                   :show-save-button="false"
+                   :show-reload-button="false"
+                   :default-layout="false"
+                   :redirect-after-edit="false"
+                   :after-load-input-data="afterLoadInputData" />
+
+      <q-card v-if="mounted"
+              class="q-mt-md">
+        <q-card-section>
+          <div class="row justify-center">
+            <div style="width: 100%;">
+              <q-chat-message v-for="message in repliesInfo"
+                              :key="message.id"
+                              :avatar="message.creator_info.picture ? message.creator_info.picture : '/assets/images/web/default-avatar.png'"
+                              :name="message.creator_info.firstname + ' ' + message.creator_info.lastname"
+                              :text="[message.body]"
+                              text-html
+                              :sent="authenticatedUser.id !== message.creator_info.id" />
+              <!--                  <div>-->
+              <!--                    <q-input v-model="replyBody"-->
+              <!--                             bottom-slots-->
+              <!--                             :loading="replyBodyLoading">-->
+              <!--                      <template v-slot:after>-->
+              <!--                        <q-btn round-->
+              <!--                               dense-->
+              <!--                               flat-->
+              <!--                               icon="send"-->
+              <!--                               :loading="replyBodyLoading"-->
+              <!--                               @click="sendReplyBody" />-->
+              <!--                      </template>-->
+              <!--                    </q-input>-->
+              <!--                  </div>-->
+            </div>
+          </div>
+        </q-card-section>
+      </q-card>
+
+      <div class="action">
+        <div class="row q-mt-lg justify-end">
+          <div class="col-md-12 col-12">
+            متن پیام
+          </div>
+          <div class="col-md-12 col-12 q-mb-md">
+            <q-input v-model="replyText"
+                     type="textarea" />
+          </div>
+          <div class="col-md-4 col-12">
+            <q-btn color="primary"
+                   class="full-width"
+                   :loading="entityLoading"
+                   @click="sendReply">
+              ارسال پاسخ
+            </q-btn>
+          </div>
+        </div>
+      </div>
+    </q-card>
+  </div>
+</template>
+
+<script>
+import { EntityShow } from 'quasar-crud'
+import { User } from 'src/models/User.js'
+import { Ticket } from 'src/models/Ticket.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { TicketCategoryList } from 'src/models/TicketCategory.js'
+
+export default {
+  name: 'ContentShow',
+  components: {
+    EntityShow
+  },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      replyText: null,
+      mounted: false,
+      entityLoading: true,
+      ticketTitle: '',
+      authenticatedUser: new User(),
+      api: APIGateway.ticket.APIAdresses.base,
+      ticketCategoryList: new TicketCategoryList(),
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Ticket.Show',
+      indexRouteName: 'Admin.Ticket.List',
+      inputs: [
+        {
+          type: 'select',
+          name: 'category',
+          responseKey: 'category',
+          placeholder: ' ',
+          options: [],
+          label: 'دسته',
+          col: 'col-md-6 col-12'
+        },
+        {
+          type: 'select',
+          name: 'status',
+          responseKey: 'status',
+          options: (new Ticket()).statusEnums,
+          multiple: false,
+          label: 'وضعیت',
+          placeholder: ' ',
+          col: 'col-md-6 col-12'
+        },
+        {
+          type: 'input',
+          name: 'title',
+          responseKey: 'title',
+          label: 'عنوان',
+          placeholder: ' ',
+          col: 'col-md-12 col-12'
+        },
+        {
+          type: 'inputEditor',
+          name: 'body',
+          responseKey: 'body',
+          label: 'متن',
+          placeholder: ' ',
+          col: 'col-md-12 col-12'
+        },
+        { type: 'hidden', name: 'id', responseKey: 'id', label: 'شناسه', col: 'col-md-12' },
+        { type: 'hidden', name: 'owner', responseKey: 'owner', label: 'owner', col: 'col-md-12' },
+        { type: 'hidden', name: 'replies_info', responseKey: 'replies_info', label: 'replies_info', col: 'col-md-12' }
+      ]
+    }
+  },
+  computed: {
+    repliesInfo () {
+      return this.inputs.find(input => input.name === 'replies_info').value
+    }
+  },
+  created() {
+    this.api = this.api + '/' + this.$route.params.id
+  },
+  mounted() {
+    this.loadOptions()
+      .then(() => {
+        this.mounted = true
+        this.loadAuthData()
+        this.$nextTick(() => {
+          this.setInputOptions('category', this.ticketCategoryList.list.map(item => {
+            return {
+              value: item.id,
+              label: item.title
+            }
+          }))
+        })
+      })
+      .catch(() => {
+        this.mounted = true
+      })
+  },
+  methods: {
+    loadAuthData () {
+      this.authenticatedUser = this.$store.getters['Auth/user']
+    },
+    afterLoadInputData (response) {
+      this.ticketTitle = response.title
+      this.entityLoading = false
+    },
+    sendReply() {
+      this.entityLoading = true
+      const body = this.replyText.replace(/\n/g, '<br>\n')
+      APIGateway.ticket.reply({
+        id: this.$route.params.id,
+        body
+      })
+        .then(() => {
+          this.$refs.entityShow.editEntity()
+            .then(() => {
+              this.replyText = null
+              this.$refs.entityShow.getData()
+              this.entityLoading = false
+            })
+            .catch(() => {
+              this.$refs.entityShow.getData()
+              this.entityLoading = false
+            })
+        })
+        .catch(() => {
+          this.$refs.entityShow.getData()
+          this.entityLoading = false
+        })
+    },
+    loadOptions () {
+      return this.loadCategories()
+    },
+    loadCategories () {
+      return new Promise((resolve, reject) => {
+        APIGateway.ticketCategory.index()
+          .then(({ list }) => {
+            this.ticketCategoryList = list
+            resolve()
+          })
+          .catch(() => {
+            reject()
+          })
+      })
+    },
+    setInputOptions (name, options) {
+      this.$refs.entityCreate.setInputAttributeByName(name, 'options', options)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.ContentShow {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    display: flex;
+    flex-flow: row;
+    position: relative;
+    .static-title {
+
+    }
+    .dynamic-title {
+
+    }
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

+ 112 - 0
src/components/Widgets/Store/StoreProduct/AdminStoreProductCreate/AdminStoreProductCreate.vue

@@ -0,0 +1,112 @@
+<template>
+  <div class="AdminContentCreate"
+       :style="localOptions.style">
+    <div class="flex justify-end">
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-create v-if="mounted"
+                   ref="entityCreate"
+                   v-model:value="inputs"
+                   title="ایجاد محتوا"
+                   :api="api"
+                   :entity-id-key="entityIdKey"
+                   :entity-param-key="entityParamKey"
+                   :show-route-name="showRouteName"
+                   :show-close-button="false"
+                   :show-edit-button="false"
+                   :show-expand-button="false"
+                   :show-save-button="false"
+                   :show-reload-button="false" />
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityCreate } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import BtnControl from 'components/Control/btn.vue'
+import ContentMedias from 'components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue'
+import ContentCategorySelector from 'components/FormBuilderCustumComponents/ContentCategorySelector.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const ContentMediasComp = shallowRef(ContentMedias)
+const ContentCategorySelectorComp = shallowRef(ContentCategorySelector)
+
+export default {
+  name: 'AdminContentCreate',
+  components: { EntityCreate },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      mounted: false,
+      entityLoading: false,
+      api: APIGateway.content.APIAdresses.base,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Content.Show',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'file', name: 'thumbnail', responseKey: 'thumbnail', label: 'عکس', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: ContentCategorySelectorComp, name: 'category', responseKey: 'category', col: 'col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', col: 'col-12' },
+        { type: ContentMediasComp, name: 'medias', responseKey: 'medias_info', col: 'col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ایجاد محتوا', placeholder: ' ', atClick: () => {}, col: 'col-md-6' }
+      ]
+    }
+  },
+  mounted() {
+    this.setActionBtn()
+    this.mounted = true
+  },
+  methods: {
+    setActionBtn () {
+      this.inputs.forEach((item, index) => {
+        if (item.name === 'btn') {
+          this.inputs[index].atClick = this.onSubmit
+        }
+      })
+    },
+    onSubmit () {
+      this.create()
+    },
+    create() {
+      this.entityLoading = true
+      this.$refs.entityCreate.createEntity()
+        .then(() => {
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminContentCreate {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    position: relative;
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Store/StoreProduct/AdminStoreProductCreate/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 148 - 0
src/components/Widgets/Store/StoreProduct/AdminStoreProductList/AdminStoreProductList.vue

@@ -0,0 +1,148 @@
+<template>
+  <div class="AdminContentList"
+       :style="localOptions.style">
+    <div v-if="showBackBtn"
+         class="more-action">
+      <q-btn label="ایجاد محصول جدید"
+             color="primary"
+             :to="{name: 'Admin.Store.Product.Create'}" />
+    </div>
+    <entity-index v-if="mounted"
+                  v-model:value="inputs"
+                  title="لیست محصولات"
+                  :api="api"
+                  :table="table"
+                  :table-keys="tableKeys"
+                  :create-route-name="createRouteName"
+                  :show-search-button="false"
+                  :show-expand-button="false"
+                  :show-reload-button="false">
+      <template #entity-index-table-cell="{inputData}">
+        <template v-if="inputData.col.name === 'number'">
+          {{ inputData.rowNumber }}
+        </template>
+        <template v-else-if="inputData.col.name === 'thumbnail'">
+          <q-img :src="inputData.col.value"
+                 width="100px" />
+        </template>
+        <template v-else-if="inputData.col.name === 'action'">
+          <q-btn color="primary"
+                 :to="{name: 'Admin.Store.Product.Show', params: {id: inputData.props.row.id}}">
+            مشاهده جزییات
+          </q-btn>
+        </template>
+        <template v-else>
+          {{ inputData.col.value }}
+        </template>
+      </template>
+    </entity-index>
+  </div>
+</template>
+
+<script>
+import { EntityIndex } from 'quasar-crud'
+import { Product } from 'src/models/Product.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'AdminContentList',
+  components: { EntityIndex },
+  mixins: [mixinWidget],
+  props: {
+    showBackBtn: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data () {
+    return {
+      api: APIGateway.product.APIAdresses.base,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [],
+      table: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'id',
+            required: true,
+            label: 'شناسه',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'نام محصول',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'is_physical_info',
+            required: true,
+            label: 'جنس',
+            align: 'left',
+            field: row => (new Product(row)).is_physical_info.label
+          },
+          {
+            name: 'content_category',
+            required: true,
+            label: 'بخش',
+            align: 'left',
+            field: row => row.content_category
+          },
+          {
+            name: 'unit_price',
+            required: true,
+            label: 'قیمت',
+            align: 'left',
+            field: row => parseInt(row.unit_price.toString()).toLocaleString('fa')
+          },
+          {
+            name: 'is_visible',
+            required: true,
+            label: 'نمایش',
+            align: 'left',
+            field: '-'
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'جزییات',
+            align: 'left',
+            field: row => ''
+          }
+        ]
+      },
+      mounted: false,
+      createRouteName: ''
+    }
+  },
+  mounted() {
+    this.mounted = true
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminContentList {
+  .more-action {
+    display: flex;
+    flex-flow: row;
+    justify-content: flex-end;
+    margin-bottom: 10px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Store/StoreProduct/AdminStoreProductList/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 141 - 0
src/components/Widgets/Store/StoreProduct/AdminStoreProductShow/AdminStoreProductShow.vue

@@ -0,0 +1,141 @@
+<template>
+  <div class="AdminContentShow"
+       :style="localOptions.style">
+    <div class="flex justify-end">
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-edit v-if="mounted"
+                 ref="entityEdit"
+                 v-model:value="inputs"
+                 title="اطلاعات محتوا"
+                 :api="api"
+                 :entity-id-key="entityIdKey"
+                 :entity-param-key="entityParamKey"
+                 :show-route-name="showRouteName"
+                 :show-close-button="false"
+                 :show-edit-button="false"
+                 :show-expand-button="false"
+                 :show-save-button="false"
+                 :show-reload-button="false"
+                 :redirect-after-edit="false"
+                 :after-load-input-data="afterLoadInputData" />
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityEdit } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import BtnControl from 'components/Control/btn.vue'
+import ProductSelector from 'components/FormBuilderCustumComponents/ProductSelector.vue'
+import ContentMedias from 'components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue'
+import ContentCategorySelector from 'components/FormBuilderCustumComponents/ContentCategorySelector.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const ContentMediasComp = shallowRef(ContentMedias)
+const ProductSelectorComp = shallowRef(ProductSelector)
+const ContentCategorySelectorComp = shallowRef(ContentCategorySelector)
+
+export default {
+  name: 'AdminContentShow',
+  components: {
+    EntityEdit
+  },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      mounted: false,
+      entityLoading: true,
+      api: null,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Content.Show',
+      indexRouteName: 'Admin.Content.List',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'file', name: 'thumbnail', responseKey: 'thumbnail', label: 'عکس', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: ContentCategorySelectorComp, name: 'content_category', responseKey: 'content_category_info', categoryType: 'content', col: 'col-md-12 col-12' },
+        { type: ContentCategorySelectorComp, name: 'store_category', responseKey: 'store_category_info', categoryType: 'store', col: 'col-md-12 col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', col: 'col-md-12 col-12' },
+        { type: 'inputEditor', name: 'specification', responseKey: 'specification', label: 'specification', col: 'col-md-12 col-12' },
+        { type: ContentMediasComp, name: 'medias', responseKey: 'medias_info', sourceType: 'PRODUCT', col: 'col-md-12 col-12' },
+        { type: 'input', name: 'unit_price', responseKey: 'unit_price', label: 'قیمت واحد', placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'input', name: 'discount_percent', responseKey: 'discount_percent', label: 'درصد تخفیف', placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'input', name: 'inventory', responseKey: 'inventory', label: 'موجودی', placeholder: ' ', col: 'col-md-2 col-12' },
+        { type: 'input', name: 'maximum_order_count', responseKey: 'maximum_order_count', label: 'سقف سفارش مجاز', placeholder: ' ', col: 'col-md-2 col-12' },
+        { type: 'input', name: 'weight', responseKey: 'weight', label: 'وزن', placeholder: ' ', col: 'col-md-2 col-12' },
+        { type: ProductSelectorComp, name: 'products', responseKey: 'products_info', label: 'انتخاب محصولات', selectionMode: 'multiple', col: 'col-12' },
+
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ویرایش محصول', placeholder: ' ', ignoreValue: true, atClick: () => {}, col: 'col-md-6' }
+      ]
+    }
+  },
+  created() {
+    this.api = APIGateway.content.APIAdresses.byId(this.$route.params.id)
+  },
+  mounted() {
+    this.setActionBtn()
+    this.mounted = true
+  },
+  methods: {
+    setActionBtn () {
+      this.inputs.forEach((item, index) => {
+        if (item.name === 'btn') {
+          this.inputs[index].atClick = this.edit
+        }
+      })
+    },
+    afterLoadInputData () {
+      this.entityLoading = false
+    },
+    edit() {
+      this.entityLoading = true
+      this.$refs.entityEdit.editEntity(false)
+        .then(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminContentShow {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    display: flex;
+    flex-flow: row;
+    position: relative;
+    .static-title {
+
+    }
+    .dynamic-title {
+
+    }
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Store/StoreProduct/AdminStoreProductShow/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 21 - 0
src/components/Widgets/Store/StoreProduct/StoreProductList/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 107 - 0
src/components/Widgets/Store/StoreProduct/StoreProductList/StoreProductList.vue

@@ -0,0 +1,107 @@
+<template>
+  <div class="ContentList"
+       :style="localOptions.style">
+    <div class="more-action">
+      <q-btn label="ایجاد درخواست"
+             color="primary"
+             :to="{name: 'UserPanel.Ticket.Create'}" />
+    </div>
+    <entity-index v-if="mounted"
+                  ref="entityIndex"
+                  v-model:value="inputs"
+                  title="لیست درخواست ها"
+                  :api="api"
+                  :table="table"
+                  :table-keys="tableKeys"
+                  :create-route-name="createRouteName"
+                  :show-search-button="false"
+                  :show-expand-button="false"
+                  :show-reload-button="false">
+      <template #entity-index-table-cell="{inputData}">
+        <template v-if="inputData.col.name === 'action'">
+          <q-btn color="primary"
+                 :to="{name: 'UserPanel.Ticket.Show', params: {id: inputData.props.row.id}}">
+            مشاهده جزییات
+          </q-btn>
+        </template>
+        <template v-else>
+          {{ inputData.col.value }}
+        </template>
+      </template>
+    </entity-index>
+  </div>
+</template>
+
+<script>
+import { EntityIndex } from 'quasar-crud'
+import { Ticket } from 'src/models/Ticket.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'ContentList',
+  components: { EntityIndex },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      api: APIGateway.ticket.APIAdresses.base,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [],
+      table: {
+        columns: [
+          {
+            name: 'title',
+            required: true,
+            label: 'موضوع پیام',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'sender',
+            required: true,
+            label: 'فرستنده',
+            align: 'left',
+            field: row => row.creator_info.firstname + ' ' + row.creator_info.lastname
+          },
+          {
+            name: 'status',
+            required: true,
+            label: 'وضعیت',
+            align: 'left',
+            field: row => (new Ticket(row)).status_info.label
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'جزییات',
+            align: 'left',
+            field: row => ''
+          }
+        ]
+      },
+      mounted: false,
+      createRouteName: ''
+    }
+  },
+  mounted() {
+    this.mounted = true
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.ContentList {
+  .more-action {
+    display: flex;
+    flex-flow: row;
+    justify-content: flex-end;
+    margin-bottom: 10px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Store/StoreProduct/StoreProductShow/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 277 - 0
src/components/Widgets/Store/StoreProduct/StoreProductShow/StoreProductShow.vue

@@ -0,0 +1,277 @@
+<template>
+  <div class="ContentShow"
+       :style="localOptions.style">
+    <div class="title">
+      <div class="static-title">
+        مشاهده پیام
+      </div>
+      <div class="dynamic-title">
+        <template v-if="entityLoading">
+          <q-skeleton type="text"
+                      class="q-ml-lg"
+                      width="100px" />
+        </template>
+        <template v-else>
+          ({{ ticketTitle }})
+        </template>
+      </div>
+      <div class="back-action">
+        <q-btn flat
+               :to="{name: 'UserPanel.Ticket.List'}"
+               color="grey">
+          بازگشت
+          >
+        </q-btn>
+      </div>
+    </div>
+    <q-card class="form"
+            flat>
+      <entity-show v-if="mounted"
+                   ref="entityShow"
+                   v-model:value="inputs"
+                   title="اطلاعات کاربری"
+                   :api="api"
+                   :entity-id-key="entityIdKey"
+                   :entity-param-key="entityParamKey"
+                   :show-route-name="showRouteName"
+                   :show-close-button="false"
+                   :show-edit-button="false"
+                   :show-expand-button="false"
+                   :show-save-button="false"
+                   :show-reload-button="false"
+                   :default-layout="false"
+                   :redirect-after-edit="false"
+                   :after-load-input-data="afterLoadInputData" />
+
+      <q-card v-if="mounted"
+              class="q-mt-md">
+        <q-card-section>
+          <div class="row justify-center">
+            <div style="width: 100%;">
+              <q-chat-message v-for="message in repliesInfo"
+                              :key="message.id"
+                              :avatar="message.creator_info.picture ? message.creator_info.picture : '/assets/images/web/default-avatar.png'"
+                              :name="message.creator_info.firstname + ' ' + message.creator_info.lastname"
+                              :text="[message.body]"
+                              text-html
+                              :sent="authenticatedUser.id !== message.creator_info.id" />
+              <!--                  <div>-->
+              <!--                    <q-input v-model="replyBody"-->
+              <!--                             bottom-slots-->
+              <!--                             :loading="replyBodyLoading">-->
+              <!--                      <template v-slot:after>-->
+              <!--                        <q-btn round-->
+              <!--                               dense-->
+              <!--                               flat-->
+              <!--                               icon="send"-->
+              <!--                               :loading="replyBodyLoading"-->
+              <!--                               @click="sendReplyBody" />-->
+              <!--                      </template>-->
+              <!--                    </q-input>-->
+              <!--                  </div>-->
+            </div>
+          </div>
+        </q-card-section>
+      </q-card>
+
+      <div class="action">
+        <div class="row q-mt-lg justify-end">
+          <div class="col-md-12 col-12">
+            متن پیام
+          </div>
+          <div class="col-md-12 col-12 q-mb-md">
+            <q-input v-model="replyText"
+                     type="textarea" />
+          </div>
+          <div class="col-md-4 col-12">
+            <q-btn color="primary"
+                   class="full-width"
+                   :loading="entityLoading"
+                   @click="sendReply">
+              ارسال پاسخ
+            </q-btn>
+          </div>
+        </div>
+      </div>
+    </q-card>
+  </div>
+</template>
+
+<script>
+import { EntityShow } from 'quasar-crud'
+import { User } from 'src/models/User.js'
+import { Ticket } from 'src/models/Ticket.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { TicketCategoryList } from 'src/models/TicketCategory.js'
+
+export default {
+  name: 'ContentShow',
+  components: {
+    EntityShow
+  },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      replyText: null,
+      mounted: false,
+      entityLoading: true,
+      ticketTitle: '',
+      authenticatedUser: new User(),
+      api: APIGateway.ticket.APIAdresses.base,
+      ticketCategoryList: new TicketCategoryList(),
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Ticket.Show',
+      indexRouteName: 'Admin.Ticket.List',
+      inputs: [
+        {
+          type: 'select',
+          name: 'category',
+          responseKey: 'category',
+          placeholder: ' ',
+          options: [],
+          label: 'دسته',
+          col: 'col-md-6 col-12'
+        },
+        {
+          type: 'select',
+          name: 'status',
+          responseKey: 'status',
+          options: (new Ticket()).statusEnums,
+          multiple: false,
+          label: 'وضعیت',
+          placeholder: ' ',
+          col: 'col-md-6 col-12'
+        },
+        {
+          type: 'input',
+          name: 'title',
+          responseKey: 'title',
+          label: 'عنوان',
+          placeholder: ' ',
+          col: 'col-md-12 col-12'
+        },
+        {
+          type: 'inputEditor',
+          name: 'body',
+          responseKey: 'body',
+          label: 'متن',
+          placeholder: ' ',
+          col: 'col-md-12 col-12'
+        },
+        { type: 'hidden', name: 'id', responseKey: 'id', label: 'شناسه', col: 'col-md-12' },
+        { type: 'hidden', name: 'owner', responseKey: 'owner', label: 'owner', col: 'col-md-12' },
+        { type: 'hidden', name: 'replies_info', responseKey: 'replies_info', label: 'replies_info', col: 'col-md-12' }
+      ]
+    }
+  },
+  computed: {
+    repliesInfo () {
+      return this.inputs.find(input => input.name === 'replies_info').value
+    }
+  },
+  created() {
+    this.api = this.api + '/' + this.$route.params.id
+  },
+  mounted() {
+    this.loadOptions()
+      .then(() => {
+        this.mounted = true
+        this.loadAuthData()
+        this.$nextTick(() => {
+          this.setInputOptions('category', this.ticketCategoryList.list.map(item => {
+            return {
+              value: item.id,
+              label: item.title
+            }
+          }))
+        })
+      })
+      .catch(() => {
+        this.mounted = true
+      })
+  },
+  methods: {
+    loadAuthData () {
+      this.authenticatedUser = this.$store.getters['Auth/user']
+    },
+    afterLoadInputData (response) {
+      this.ticketTitle = response.title
+      this.entityLoading = false
+    },
+    sendReply() {
+      this.entityLoading = true
+      const body = this.replyText.replace(/\n/g, '<br>\n')
+      APIGateway.ticket.reply({
+        id: this.$route.params.id,
+        body
+      })
+        .then(() => {
+          this.$refs.entityShow.editEntity()
+            .then(() => {
+              this.replyText = null
+              this.$refs.entityShow.getData()
+              this.entityLoading = false
+            })
+            .catch(() => {
+              this.$refs.entityShow.getData()
+              this.entityLoading = false
+            })
+        })
+        .catch(() => {
+          this.$refs.entityShow.getData()
+          this.entityLoading = false
+        })
+    },
+    loadOptions () {
+      return this.loadCategories()
+    },
+    loadCategories () {
+      return new Promise((resolve, reject) => {
+        APIGateway.ticketCategory.index()
+          .then(({ list }) => {
+            this.ticketCategoryList = list
+            resolve()
+          })
+          .catch(() => {
+            reject()
+          })
+      })
+    },
+    setInputOptions (name, options) {
+      this.$refs.entityCreate.setInputAttributeByName(name, 'options', options)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.ContentShow {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    display: flex;
+    flex-flow: row;
+    position: relative;
+    .static-title {
+
+    }
+    .dynamic-title {
+
+    }
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

+ 1 - 1
src/layouts/MainLayout.vue

@@ -61,7 +61,7 @@ import AuthLogin from 'src/components/Auth.vue'
 import FooterTemplate from 'src/components/Template/Footer/FooterTemplate.vue'
 import KeepAliveComponents from 'src/assets/js/KeepAliveComponents.js'
 import templateHeader from 'src/components/Template/Header/TemplateHeader.vue'
-import TemplateSideBar from 'src/components/Template/SideBard/TemplateSideBar.vue'
+import TemplateSideBar from 'src/components/Template/SideBar/TemplateSideBar.vue'
 import QuasarTemplateBuilder from 'quasar-template-builder/src/quasar-template-builder.vue'
 import FloatingActionButton from 'components/Template/FloatingActionButton/FloatingActionButton.vue'
 

+ 17 - 1
src/models/Package.js

@@ -13,6 +13,7 @@ class Package extends Model {
       { key: 'unit_price' },
       { key: 'discount_percent' },
       { key: 'description' },
+      { key: 'is_physical' },
       { key: 'maximum_order_count' },
       { key: 'weight' },
       { key: 'specification' },
@@ -21,7 +22,22 @@ class Package extends Model {
         key: 'products_info',
         relatedModel: ProductList
       }
-    ])
+    ], {
+      is_physical: {
+        infoKey: 'is_physical_info',
+        enumListKey: 'is_physicalEnums',
+        enums: [
+          {
+            label: 'فیزیکی',
+            value: true
+          },
+          {
+            label: 'مجازی',
+            value: false
+          }
+        ]
+      }
+    })
 
     if (!this.thumbnail) {
       this.thumbnail = '/img/sample-post-thumbnail.jpg'

+ 14 - 0
src/models/Product.js

@@ -18,6 +18,20 @@ class Product extends Model {
       { key: 'physical_type' },
       { key: 'specification' }
     ], {
+      is_physical: {
+        infoKey: 'is_physical_info',
+        enumListKey: 'is_physicalEnums',
+        enums: [
+          {
+            label: 'فیزیکی',
+            value: true
+          },
+          {
+            label: 'مجازی',
+            value: false
+          }
+        ]
+      },
       physical_type: {
         infoKey: 'physical_type_info',
         enumListKey: 'physical_typeEnums',

+ 23 - 0
src/models/StoreCategory.js

@@ -0,0 +1,23 @@
+import { Collection, Model } from 'js-abstract-model'
+
+class StoreCategory extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      { key: 'title' },
+      { key: 'parent' },
+      {
+        key: 'children',
+        defalut: []
+      }
+    ])
+  }
+}
+
+class StoreCategoryList extends Collection {
+  model () {
+    return StoreCategory
+  }
+}
+
+export { StoreCategory, StoreCategoryList }

+ 23 - 0
src/pages/Admin/StoreCategory/Create.vue

@@ -0,0 +1,23 @@
+<template>
+  <q-page-builder v-model:sections="currenSections"
+                  v-model:options="pageConfig"
+                  :editable="pageBuilderEditable"
+                  :loading="pageBuilderLoading" />
+</template>
+
+<script>
+import { mixinPageOptions } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'Admin.Store.Category.Create',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminStoreCategoryCreate' }], options: {} }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '10px', alignment: { justifyContent: { global: null, xl: null, lg: null, md: null, sm: null, xs: null }, alignItems: { global: null, xl: null, lg: null, md: null, sm: null, xs: null }, alignContent: { global: null, xl: null, lg: null, md: null, sm: null, xs: null } }, style: { maxWidth: '1200px', width: '1200px' } } }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 23 - 0
src/pages/Admin/StoreCategory/List.vue

@@ -0,0 +1,23 @@
+<template>
+  <q-page-builder v-model:sections="currenSections"
+                  v-model:options="pageConfig"
+                  :editable="pageBuilderEditable"
+                  :loading="pageBuilderLoading" />
+</template>
+
+<script>
+import { mixinPageOptions } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'Admin.Store.Category.List',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminStoreCategoryList' }], options: {} }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '10px', alignment: { justifyContent: { global: null, xl: null, lg: null, md: null, sm: null, xs: null }, alignItems: { global: null, xl: null, lg: null, md: null, sm: null, xs: null }, alignContent: { global: null, xl: null, lg: null, md: null, sm: null, xs: null } }, style: { maxWidth: '1200px', width: '1200px' } } }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 23 - 0
src/pages/Admin/StoreCategory/Show.vue

@@ -0,0 +1,23 @@
+<template>
+  <q-page-builder v-model:sections="currenSections"
+                  v-model:options="pageConfig"
+                  :editable="pageBuilderEditable"
+                  :loading="pageBuilderLoading" />
+</template>
+
+<script>
+import { mixinPageOptions } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'Admin.Store.Category.Show',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminStoreCategoryShow' }], options: {} }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '10px', alignment: { justifyContent: { global: null, xl: null, lg: null, md: null, sm: null, xs: null }, alignItems: { global: null, xl: null, lg: null, md: null, sm: null, xs: null }, alignContent: { global: null, xl: null, lg: null, md: null, sm: null, xs: null } }, style: { maxWidth: '1200px', width: '1200px' } } }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 23 - 0
src/pages/Admin/StoreProduct/Create.vue

@@ -0,0 +1,23 @@
+<template>
+  <q-page-builder v-model:sections="currenSections"
+                  v-model:options="pageConfig"
+                  :editable="pageBuilderEditable"
+                  :loading="pageBuilderLoading" />
+</template>
+
+<script>
+import { mixinPageOptions } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'Admin.Store.Product.Create',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminStoreProductCreate' }], options: {} }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '10px', alignment: { justifyContent: { global: null, xl: null, lg: null, md: null, sm: null, xs: null }, alignItems: { global: null, xl: null, lg: null, md: null, sm: null, xs: null }, alignContent: { global: null, xl: null, lg: null, md: null, sm: null, xs: null } }, style: { maxWidth: '1200px', width: '1200px' } } }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 23 - 0
src/pages/Admin/StoreProduct/List.vue

@@ -0,0 +1,23 @@
+<template>
+  <q-page-builder v-model:sections="currenSections"
+                  v-model:options="pageConfig"
+                  :editable="pageBuilderEditable"
+                  :loading="pageBuilderLoading" />
+</template>
+
+<script>
+import { mixinPageOptions } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'Admin.Store.Product.List',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminStoreProductList' }], options: {} }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '10px', alignment: { justifyContent: { global: null, xl: null, lg: null, md: null, sm: null, xs: null }, alignItems: { global: null, xl: null, lg: null, md: null, sm: null, xs: null }, alignContent: { global: null, xl: null, lg: null, md: null, sm: null, xs: null } }, style: { maxWidth: '1200px', width: '1200px' } } }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 23 - 0
src/pages/Admin/StoreProduct/Show.vue

@@ -0,0 +1,23 @@
+<template>
+  <q-page-builder v-model:sections="currenSections"
+                  v-model:options="pageConfig"
+                  :editable="pageBuilderEditable"
+                  :loading="pageBuilderLoading" />
+</template>
+
+<script>
+import { mixinPageOptions } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'Admin.Store.Product.Show',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminStoreProductShow' }], options: {} }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '10px', alignment: { justifyContent: { global: null, xl: null, lg: null, md: null, sm: null, xs: null }, alignItems: { global: null, xl: null, lg: null, md: null, sm: null, xs: null }, alignContent: { global: null, xl: null, lg: null, md: null, sm: null, xs: null } }, style: { maxWidth: '1200px', width: '1200px' } } }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 37 - 0
src/router/routes/Admin.js

@@ -354,6 +354,43 @@ export default {
         }
       ]
     },
+    {
+      path: 'store',
+      name: 'Admin.Store',
+      component: () => import('layouts/bareLayout.vue'),
+      children: [
+        {
+          path: 'category',
+          name: 'Admin.Store.Category',
+          component: () => import('layouts/bareLayout.vue'),
+          children: [
+            { path: '', name: 'Admin.Store.Category.List', component: () => import('src/pages/Admin/StoreCategory/List.vue') },
+            { path: 'create', name: 'Admin.Store.Category.Create', component: () => import('src/pages/Admin/StoreCategory/Create.vue') },
+            { path: ':id', name: 'Admin.Store.Category.Show', component: () => import('src/pages/Admin/StoreCategory/Show.vue') }
+          ]
+        },
+        {
+          path: 'package',
+          name: 'Admin.Store.Package',
+          component: () => import('layouts/bareLayout.vue'),
+          children: [
+            { path: '', name: 'Admin.Store.Package.List', component: () => import('src/pages/Admin/StorePackage/List.vue') },
+            { path: 'create', name: 'Admin.Store.Package.Create', component: () => import('src/pages/Admin/StorePackage/Create.vue') },
+            { path: ':id', name: 'Admin.Store.Package.Show', component: () => import('src/pages/Admin/StorePackage/Show.vue') }
+          ]
+        },
+        {
+          path: 'product',
+          name: 'Admin.Store.Product',
+          component: () => import('layouts/bareLayout.vue'),
+          children: [
+            { path: '', name: 'Admin.Store.Product.List', component: () => import('src/pages/Admin/StoreProduct/List.vue') },
+            { path: 'create', name: 'Admin.Store.Product.Create', component: () => import('src/pages/Admin/StoreProduct/Create.vue') },
+            { path: ':id', name: 'Admin.Store.Product.Show', component: () => import('src/pages/Admin/StoreProduct/Show.vue') }
+          ]
+        }
+      ]
+    },
     {
       path: 'transaction',
       name: 'Admin.Transaction',

+ 8 - 0
src/router/routes/Public.js

@@ -80,6 +80,14 @@ export default {
       },
       component: () => import('src/pages/Public/FAQ.vue')
     },
+    {
+      name: 'Public.Shop',
+      path: 'shop',
+      meta: {
+        hasDynamicSetting: true
+      },
+      component: () => import('src/pages/BasicDynamicPage.vue')
+    },
     {
       name: 'Public.Rules',
       path: 'rules',

+ 177 - 22
yarn.lock

@@ -271,6 +271,11 @@
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.3.tgz#0ce0be31a4ca4f1884b5786057cadcb6c3be58f9"
   integrity sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==
 
+"@babel/parser@^7.24.7":
+  version "7.24.7"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.7.tgz#9a5226f92f0c5c8ead550b750f5608e766c8ce85"
+  integrity sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==
+
 "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.23.3":
   version "7.23.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz#5cd1c87ba9380d0afb78469292c954fee5d2411a"
@@ -1271,6 +1276,11 @@
   resolved "https://registry.yarnpkg.com/@quasar/extras/-/extras-1.16.8.tgz#be93a0e2d465425f1d85e9fc9a09096bf2845aaf"
   integrity sha512-mNSwQ6wkIVoAemhYdLm9q/FVqLHmVONZgCYdLLeqpakKNsR0r/zHQnCngzdDaXI7veYEvE0uW1gJa1wdQsxXxA==
 
+"@quasar/extras@^1.16.11":
+  version "1.16.11"
+  resolved "https://registry.yarnpkg.com/@quasar/extras/-/extras-1.16.11.tgz#84b1efb9097a6e58c3ebfdd5da83ac658056a35c"
+  integrity sha512-sbTBHOA+Hi7ah0P6qSm+xfRXqwJ94ct3NKA3Lkq3iNPYuHD7VXbSWtP2eA7Cu9Fd0WjVoPbngf6yFGg46U3IfQ==
+
 "@quasar/extras@^1.16.9":
   version "1.16.9"
   resolved "https://registry.yarnpkg.com/@quasar/extras/-/extras-1.16.9.tgz#6cdf9d34862e6f58009443a2d1e34b84a5e73ad0"
@@ -1837,6 +1847,17 @@
     estree-walker "^2.0.2"
     source-map-js "^1.0.2"
 
+"@vue/compiler-core@3.4.31":
+  version "3.4.31"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.31.tgz#b51a76f1b30e9b5eba0553264dff0f171aedb7c6"
+  integrity sha512-skOiodXWTV3DxfDhB4rOf3OGalpITLlgCeOwb+Y9GJpfQ8ErigdBUHomBzvG78JoVE8MJoQsb+qhZiHfKeNeEg==
+  dependencies:
+    "@babel/parser" "^7.24.7"
+    "@vue/shared" "3.4.31"
+    entities "^4.5.0"
+    estree-walker "^2.0.2"
+    source-map-js "^1.2.0"
+
 "@vue/compiler-dom@3.3.8":
   version "3.3.8"
   resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.3.8.tgz#09d832514b9b8d9415a3816b065d69dbefcc7e9b"
@@ -1845,6 +1866,14 @@
     "@vue/compiler-core" "3.3.8"
     "@vue/shared" "3.3.8"
 
+"@vue/compiler-dom@3.4.31":
+  version "3.4.31"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.31.tgz#30961ca847f5d6ad18ffa26236c219f61b195f6b"
+  integrity sha512-wK424WMXsG1IGMyDGyLqB+TbmEBFM78hIsOJ9QwUVLGrcSk0ak6zYty7Pj8ftm7nEtdU/DGQxAXp0/lM/2cEpQ==
+  dependencies:
+    "@vue/compiler-core" "3.4.31"
+    "@vue/shared" "3.4.31"
+
 "@vue/compiler-sfc@3.3.8", "@vue/compiler-sfc@^3.2.47":
   version "3.3.8"
   resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.3.8.tgz#40b18e48aa00260950964d1d72157668521be0e1"
@@ -1861,6 +1890,21 @@
     postcss "^8.4.31"
     source-map-js "^1.0.2"
 
+"@vue/compiler-sfc@3.4.31":
+  version "3.4.31"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.31.tgz#cc6bfccda17df8268cc5440842277f61623c591f"
+  integrity sha512-einJxqEw8IIJxzmnxmJBuK2usI+lJonl53foq+9etB2HAzlPjAS/wa7r0uUpXw5ByX3/0uswVSrjNb17vJm1kQ==
+  dependencies:
+    "@babel/parser" "^7.24.7"
+    "@vue/compiler-core" "3.4.31"
+    "@vue/compiler-dom" "3.4.31"
+    "@vue/compiler-ssr" "3.4.31"
+    "@vue/shared" "3.4.31"
+    estree-walker "^2.0.2"
+    magic-string "^0.30.10"
+    postcss "^8.4.38"
+    source-map-js "^1.2.0"
+
 "@vue/compiler-ssr@3.3.8":
   version "3.3.8"
   resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.3.8.tgz#136eed54411e4694815d961048a237191063fbce"
@@ -1869,6 +1913,14 @@
     "@vue/compiler-dom" "3.3.8"
     "@vue/shared" "3.3.8"
 
+"@vue/compiler-ssr@3.4.31":
+  version "3.4.31"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.31.tgz#f62ffecdf15bacb883d0099780cf9a1e3654bfc4"
+  integrity sha512-RtefmITAje3fJ8FSg1gwgDhdKhZVntIVbwupdyZDSifZTRMiWxWehAOTCc8/KZDnBOcYQ4/9VWxsTbd3wT0hAA==
+  dependencies:
+    "@vue/compiler-dom" "3.4.31"
+    "@vue/shared" "3.4.31"
+
 "@vue/devtools-api@^6.0.0-beta.11", "@vue/devtools-api@^6.5.0":
   version "6.5.1"
   resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.5.1.tgz#7f71f31e40973eeee65b9a64382b13593fdbd697"
@@ -1892,6 +1944,13 @@
   dependencies:
     "@vue/shared" "3.3.8"
 
+"@vue/reactivity@3.4.31":
+  version "3.4.31"
+  resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.31.tgz#eda80e90c4f9d7659efe1f5ed99c2dfdc9e93d77"
+  integrity sha512-VGkTani8SOoVkZNds1PfJ/T1SlAIOf8E58PGAhIOUDYPC4GAmFA2u/E14TDAFcf3vVDKunc4QqCe/SHr8xC65Q==
+  dependencies:
+    "@vue/shared" "3.4.31"
+
 "@vue/runtime-core@3.3.8":
   version "3.3.8"
   resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.3.8.tgz#fba5a632cbf2b5d29e171489570149cb6975dcdb"
@@ -1900,6 +1959,14 @@
     "@vue/reactivity" "3.3.8"
     "@vue/shared" "3.3.8"
 
+"@vue/runtime-core@3.4.31":
+  version "3.4.31"
+  resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.4.31.tgz#ad3a41ad76385c0429e3e4dbefb81918494e10cf"
+  integrity sha512-LDkztxeUPazxG/p8c5JDDKPfkCDBkkiNLVNf7XZIUnJ+66GVGkP+TIh34+8LtPisZ+HMWl2zqhIw0xN5MwU1cw==
+  dependencies:
+    "@vue/reactivity" "3.4.31"
+    "@vue/shared" "3.4.31"
+
 "@vue/runtime-dom@3.3.8":
   version "3.3.8"
   resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.3.8.tgz#e2d7aa795cf50914dda9a951887765a594b38af4"
@@ -1909,6 +1976,16 @@
     "@vue/shared" "3.3.8"
     csstype "^3.1.2"
 
+"@vue/runtime-dom@3.4.31":
+  version "3.4.31"
+  resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.4.31.tgz#bae7ad844f944af33699c73581bc36125bab96ce"
+  integrity sha512-2Auws3mB7+lHhTFCg8E9ZWopA6Q6L455EcU7bzcQ4x6Dn4cCPuqj6S2oBZgN2a8vJRS/LSYYxwFFq2Hlx3Fsaw==
+  dependencies:
+    "@vue/reactivity" "3.4.31"
+    "@vue/runtime-core" "3.4.31"
+    "@vue/shared" "3.4.31"
+    csstype "^3.1.3"
+
 "@vue/server-renderer@3.3.8":
   version "3.3.8"
   resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.3.8.tgz#9b1779010e75783edeed8fcfb97d9c95fc3ac5d2"
@@ -1917,11 +1994,24 @@
     "@vue/compiler-ssr" "3.3.8"
     "@vue/shared" "3.3.8"
 
+"@vue/server-renderer@3.4.31":
+  version "3.4.31"
+  resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.4.31.tgz#bbe990f793c36d62d05bdbbaf142511d53e159fd"
+  integrity sha512-D5BLbdvrlR9PE3by9GaUp1gQXlCNadIZytMIb8H2h3FMWJd4oUfkUTEH2wAr3qxoRz25uxbTcbqd3WKlm9EHQA==
+  dependencies:
+    "@vue/compiler-ssr" "3.4.31"
+    "@vue/shared" "3.4.31"
+
 "@vue/shared@3.3.8":
   version "3.3.8"
   resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.3.8.tgz#f044942142e1d3a395f24132e6203a784838542d"
   integrity sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==
 
+"@vue/shared@3.4.31":
+  version "3.4.31"
+  resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.31.tgz#af9981f57def2c3f080c14bf219314fc0dc808a0"
+  integrity sha512-Yp3wtJk//8cO4NItOPpi3QkLExAr/aLBGZMmTtW9WpdwBCJpRM6zj9WgWktXAl8IDIozwNMByT45JP3tO3ACWA==
+
 "@xmldom/xmldom@^0.8.3", "@xmldom/xmldom@^0.8.8":
   version "0.8.10"
   resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99"
@@ -2209,12 +2299,12 @@ axios@^1.6.2:
     form-data "^4.0.0"
     proxy-from-env "^1.1.0"
 
-axios@^1.6.5:
-  version "1.6.5"
-  resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.5.tgz#2c090da14aeeab3770ad30c3a1461bc970fb0cd8"
-  integrity sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==
+axios@^1.6.8:
+  version "1.7.2"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621"
+  integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==
   dependencies:
-    follow-redirects "^1.15.4"
+    follow-redirects "^1.15.6"
     form-data "^4.0.0"
     proxy-from-env "^1.1.0"
 
@@ -2709,6 +2799,11 @@ csstype@^3.1.2:
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
   integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
 
+csstype@^3.1.3:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
+  integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
+
 dash-get@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/dash-get/-/dash-get-1.0.2.tgz#4c9e9ad5ef04c4bf9d3c9a451f6f7997298dcc7c"
@@ -2907,6 +3002,11 @@ engine.io-parser@~5.2.1:
   resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.1.tgz#9f213c77512ff1a6cc0c7a86108a7ffceb16fcfb"
   integrity sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==
 
+entities@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
+  integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
+
 entities@~3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4"
@@ -3690,10 +3790,10 @@ follow-redirects@^1.15.0:
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a"
   integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==
 
-follow-redirects@^1.15.4:
-  version "1.15.4"
-  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
-  integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
+follow-redirects@^1.15.6:
+  version "1.15.6"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
+  integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
 
 for-each@^0.3.3:
   version "0.3.3"
@@ -4391,6 +4491,11 @@ jalaali-js@^1.1.0:
   resolved "https://registry.yarnpkg.com/jalaali-js/-/jalaali-js-1.2.6.tgz#f4ee4bf686ed32bb9656f101225be4b7a1c3fd21"
   integrity sha512-io974va+Qyu+UfuVX3UIAgJlxLhAMx9Y8VMfh+IG00Js7hXQo1qNQuwSiSa0xxco0SVgx5HWNkaiCcV+aZ8WPw==
 
+jalaali-js@^1.2.6:
+  version "1.2.7"
+  resolved "https://registry.yarnpkg.com/jalaali-js/-/jalaali-js-1.2.7.tgz#7694cab2d0ca21830766faf08278e71c204cc4fe"
+  integrity sha512-gE+YHWSbygYAoJa+Xg8LWxGILqFOxZSBQQw39ghel01fVFUxV7bjL0x1JFsHcLQ3uPjvn81HQMa+kxwyPWnxGQ==
+
 jest-worker@^26.2.1:
   version "26.6.2"
   resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed"
@@ -4713,6 +4818,13 @@ magic-string@^0.25.0, magic-string@^0.25.7:
   dependencies:
     sourcemap-codec "^1.4.8"
 
+magic-string@^0.30.10:
+  version "0.30.10"
+  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e"
+  integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==
+  dependencies:
+    "@jridgewell/sourcemap-codec" "^1.4.15"
+
 magic-string@^0.30.5:
   version "0.30.5"
   resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9"
@@ -4918,6 +5030,11 @@ moment@^2.19.3, moment@^2.22.2, moment@^2.29.4:
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
   integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
 
+moment@^2.30.1:
+  version "2.30.1"
+  resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae"
+  integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==
+
 mpd-parser@0.22.1, mpd-parser@^0.22.1:
   version "0.22.1"
   resolved "https://registry.yarnpkg.com/mpd-parser/-/mpd-parser-0.22.1.tgz#bc2bf7d3e56368e4b0121035b055675401871521"
@@ -5002,7 +5119,7 @@ nan@^2.14.0:
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.18.0.tgz#26a6faae7ffbeb293a39660e88a76b82e30b7554"
   integrity sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==
 
-nanoid@^3.3.6:
+nanoid@^3.3.6, nanoid@^3.3.7:
   version "3.3.7"
   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
   integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
@@ -5303,6 +5420,11 @@ picocolors@^1.0.0:
   resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
   integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
 
+picocolors@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1"
+  integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==
+
 picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
@@ -5353,6 +5475,15 @@ postcss@^8.4.13, postcss@^8.4.21, postcss@^8.4.31:
     picocolors "^1.0.0"
     source-map-js "^1.0.2"
 
+postcss@^8.4.38:
+  version "8.4.39"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.39.tgz#aa3c94998b61d3a9c259efa51db4b392e1bde0e3"
+  integrity sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==
+  dependencies:
+    nanoid "^3.3.7"
+    picocolors "^1.0.1"
+    source-map-js "^1.2.0"
+
 prelude-ls@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@@ -5572,28 +5703,31 @@ quasar-app-extension-q-page-builder@^0.4.4:
   dependencies:
     quasar-ui-q-page-builder "^0.4.4"
 
-quasar-crud@^1.0.9:
-  version "1.0.9"
-  resolved "https://registry.yarnpkg.com/quasar-crud/-/quasar-crud-1.0.9.tgz#cca0e15483344f7d5a9d2d3d9acab2691fd3bae7"
-  integrity sha512-guiPK899Z6LetlEH0FUEK8V/VvNEkjHs3td84whaLkh5u1Nk0jLEpLFmr5PK36pomyZypx0L64ISjxk5WyyZIw==
+quasar-crud@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/quasar-crud/-/quasar-crud-2.1.0.tgz#2c1d6793ab99ef932039dcd035af27b677ffe677"
+  integrity sha512-grj3ymLU66VS9KWkAInK/oOaX9aY2TQ6v8BGKjj27ftmOB0I/Es+J+tiE2Wl5p1GEhBv5918cRHwUWNpOb7nsQ==
   dependencies:
-    "@quasar/extras" "^1.16.9"
-    axios "^1.6.5"
+    "@quasar/extras" "^1.16.11"
+    axios "^1.6.8"
     moment-jalaali "^0.10.0"
-    quasar "^2.14.2"
-    quasar-form-builder "^1.0.6"
+    quasar "^2.15.2"
+    quasar-form-builder "^2.4.1"
     shvl "^3.0.0"
 
-quasar-form-builder@^1.0.6:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/quasar-form-builder/-/quasar-form-builder-1.0.6.tgz#3f8caab7c1544d9c74f10a0bf34684703a39e7a6"
-  integrity sha512-Q7DHlVUsDmk3EMpBfE1K9Ih1VHeMYcGkTjvkqDa4MS2yBUa23glMZhjlLrF0h+SM+agcyad9WsAd1CQSjbE5Hw==
+quasar-form-builder@^2.4.1:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/quasar-form-builder/-/quasar-form-builder-2.4.1.tgz#3e5e0c41cd52caec5b792b263dc4055e7099a4ba"
+  integrity sha512-TtZOGy9pj4fJkxqGq/zg0YGpIZ762bNxXKRgf4thCjCbUjRTbbHfD+xHImTQvttW4Psgn/eqjjUhjuYsGL55uQ==
   dependencies:
     "@quasar/extras" "^1.16.9"
     core-js "^3.34.0"
+    jalaali-js "^1.2.6"
+    moment "^2.30.1"
     moment-jalaali "^0.10.0"
     quasar "^2.14.2"
     shvl "^3.0.0"
+    vue "^3.4.21"
 
 quasar-template-builder@^1.0.23:
   version "1.0.23"
@@ -5626,6 +5760,11 @@ quasar@^2.14.2:
   resolved "https://registry.yarnpkg.com/quasar/-/quasar-2.14.2.tgz#794d8434eb449549fc8455aaec9bc14b71a57c98"
   integrity sha512-f5KliWtM5BEuFsDU4yvuP+dlVIWZNrGu5VpWFsxzjpoykcP4B2HIOUiCl3mx2NCqERHd4Ts0aeioRkt9TTeExA==
 
+quasar@^2.15.2:
+  version "2.16.5"
+  resolved "https://registry.yarnpkg.com/quasar/-/quasar-2.16.5.tgz#0155ea2155683defa8767aa777671ac162e26de8"
+  integrity sha512-9pjKOg/ZpbbBaKDLIkIKgd2JLaX+R3H2jKDPrlkHD56rp7L8WdxFZhxnG5JVfvW94pCNGtlepjgnli8wRhVFCQ==
+
 queue-microtask@^1.2.2:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
@@ -6139,6 +6278,11 @@ socket.io-parser@~4.2.4:
   resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
   integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
 
+source-map-js@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
+  integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
+
 source-map-support@~0.5.20:
   version "0.5.21"
   resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
@@ -7026,6 +7170,17 @@ vue@^3.2.37, vue@^3.2.47, vue@^3.3.7:
     "@vue/server-renderer" "3.3.8"
     "@vue/shared" "3.3.8"
 
+vue@^3.4.21:
+  version "3.4.31"
+  resolved "https://registry.yarnpkg.com/vue/-/vue-3.4.31.tgz#83a3c4dab8302b0e974b0d4b92a2f6a6378ae797"
+  integrity sha512-njqRrOy7W3YLAlVqSKpBebtZpDVg21FPoaq1I7f/+qqBThK9ChAIjkRWgeP6Eat+8C+iia4P3OYqpATP21BCoQ==
+  dependencies:
+    "@vue/compiler-dom" "3.4.31"
+    "@vue/compiler-sfc" "3.4.31"
+    "@vue/runtime-dom" "3.4.31"
+    "@vue/server-renderer" "3.4.31"
+    "@vue/shared" "3.4.31"
+
 vuex-persistedstate@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/vuex-persistedstate/-/vuex-persistedstate-4.1.0.tgz#127165f85f5b4534fb3170a5d3a8be9811bd2a53"