소스 검색

admin content and media

ali 3 년 전
부모
커밋
945eb3b174
56개의 변경된 파일과 2517개의 추가작업 그리고 368개의 파일을 삭제
  1. 4 4
      package.json
  2. 10 0
      src/api/APIGateway.js
  3. 55 0
      src/api/models/Content.js
  4. 57 0
      src/api/models/ContentCategory.js
  5. 55 0
      src/api/models/Media.js
  6. 54 0
      src/api/models/MediaCategory.js
  7. 54 0
      src/api/models/MediaHashtag.js
  8. 10 15
      src/api/models/PageSetting.js
  9. 2 0
      src/boot/registerQPageBuilder.js
  10. 135 0
      src/components/FormBuilderCustumComponents/ContentCategorySelector.vue
  11. 124 0
      src/components/FormBuilderCustumComponents/ContentMedias.vue
  12. 0 84
      src/components/FormBuilderCustumComponents/Map/ActivityType.vue
  13. 0 79
      src/components/FormBuilderCustumComponents/Map/ItemEntity.vue
  14. 0 93
      src/components/FormBuilderCustumComponents/Map/LineType.vue
  15. 86 0
      src/components/ImageUploader.vue
  16. 0 3
      src/components/Template/FloatingActionButton/FloatingActionButton.vue
  17. 37 3
      src/components/Template/SideBard/AdminPanelSideBar.vue
  18. 104 0
      src/components/Widgets/Content/AdminContentCreate/AdminContentCreate.vue
  19. 21 0
      src/components/Widgets/Content/AdminContentCreate/OptionPanel.vue
  20. 106 0
      src/components/Widgets/Content/AdminContentList/AdminContentList.vue
  21. 21 0
      src/components/Widgets/Content/AdminContentList/OptionPanel.vue
  22. 127 0
      src/components/Widgets/Content/AdminContentShow/AdminContentShow.vue
  23. 21 0
      src/components/Widgets/Content/AdminContentShow/OptionPanel.vue
  24. 107 0
      src/components/Widgets/Content/ContentList/ContentList.vue
  25. 21 0
      src/components/Widgets/Content/ContentList/OptionPanel.vue
  26. 277 0
      src/components/Widgets/Content/ContentShow/ContentShow.vue
  27. 21 0
      src/components/Widgets/Content/ContentShow/OptionPanel.vue
  28. 113 0
      src/components/Widgets/ContentCategory/AdminContentCategoryCreate/AdminContentCategoryCreate.vue
  29. 21 0
      src/components/Widgets/ContentCategory/AdminContentCategoryCreate/OptionPanel.vue
  30. 120 0
      src/components/Widgets/ContentCategory/AdminContentCategoryList/AdminContentCategoryList.vue
  31. 21 0
      src/components/Widgets/ContentCategory/AdminContentCategoryList/OptionPanel.vue
  32. 117 0
      src/components/Widgets/ContentCategory/AdminContentCategoryShow/AdminContentCategoryShow.vue
  33. 21 0
      src/components/Widgets/ContentCategory/AdminContentCategoryShow/OptionPanel.vue
  34. 21 50
      src/components/Widgets/Ticket/AdminTicketList/AdminTicketList.vue
  35. 19 0
      src/models/Celebrity.js
  36. 50 0
      src/models/Content.js
  37. 23 0
      src/models/ContentCategory.js
  38. 19 0
      src/models/ContentHashtag.js
  39. 72 0
      src/models/Media.js
  40. 23 0
      src/models/MediaCategory.js
  41. 19 0
      src/models/MediaHashtag.js
  42. 23 0
      src/pages/Admin/Classroom/Ticket.vue
  43. 23 0
      src/pages/Admin/Content/Create.vue
  44. 23 0
      src/pages/Admin/Content/List.vue
  45. 23 0
      src/pages/Admin/Content/Show.vue
  46. 23 0
      src/pages/Admin/Content/Ticket.vue
  47. 23 0
      src/pages/Admin/ContentCategory/Create.vue
  48. 23 0
      src/pages/Admin/ContentCategory/List.vue
  49. 23 0
      src/pages/Admin/ContentCategory/Show.vue
  50. 23 0
      src/pages/Admin/Ticket/Create.vue
  51. 23 0
      src/pages/Admin/Ticket/List.vue
  52. 23 0
      src/pages/Admin/Ticket/Show.vue
  53. 45 1
      src/router/routes.js
  54. 13 8
      src/store/PageBuilder/actions.js
  55. 1 1
      src/store/PageBuilder/state.js
  56. 37 27
      yarn.lock

+ 4 - 4
package.json

@@ -50,11 +50,11 @@
     "postcss-rtlcss": "^4.0.4",
     "process": "^0.11.10",
     "quasar": "^2.12.0",
-    "quasar-app-extension-q-page-builder": "^0.1.49",
-    "quasar-crud": "^0.3.31",
-    "quasar-form-builder": "^0.1.98",
+    "quasar-app-extension-q-page-builder": "^0.1.51",
+    "quasar-crud": "^0.3.42",
+    "quasar-form-builder": "^0.1.106",
     "quasar-template-builder": "^1.0.23",
-    "quasar-ui-q-page-builder": "^0.1.49",
+    "quasar-ui-q-page-builder": "^0.1.51",
     "socket.io-client": "^4.6.1",
     "sticky-sidebar-v2": "^1.1.1",
     "video.js": "^8.3.0",

+ 10 - 0
src/api/APIGateway.js

@@ -2,18 +2,23 @@ import SeoAPI from 'src/api/models/Seo.js'
 import AuthAPI from 'src/api/models/Auth.js'
 import UserAPI from 'src/api/models/User.js'
 import UnitAPI from 'src/api/models/Unit.js'
+import MediaAPI from 'src/api/models/Media.js'
 import TicketAPI from 'src/api/models/Ticket.js'
 import WalletAPI from 'src/api/models/Wallet.js'
 import InvoiceAPI from 'src/api/models/Invoice.js'
 import PaymentAPI from 'src/api/models/Payment.js'
 import SessionAPI from 'src/api/models/Session.js'
+import ContentAPI from 'src/api/models/Content.js'
 import ClassroomAPI from 'src/api/models/Classroom.js'
 import TransactionAPI from 'src/api/models/Transaction.js'
 import PageSettingAPI from 'src/api/models/PageSetting.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 MediaCategoryAPI from 'src/api/models/MediaCategory.js'
 import TicketCategoryAPI from 'src/api/models/TicketCategory.js'
 import SessionTemplateAPI from 'src/api/models/SessionTemplate.js'
+import ContentCategoryAPI from 'src/api/models/ContentCategory.js'
 import ClassroomRegistrationAPI from 'src/api/models/ClassroomRegistration.js'
 
 /* Exporting the APIGateway object. */
@@ -22,17 +27,22 @@ export const APIGateway = {
   auth: new AuthAPI(),
   user: new UserAPI(),
   unit: new UnitAPI(),
+  media: new MediaAPI(),
   ticket: new TicketAPI(),
   wallet: new WalletAPI(),
   invoice: new InvoiceAPI(),
   payment: new PaymentAPI(),
   session: new SessionAPI(),
+  content: new ContentAPI(),
   classroom: new ClassroomAPI(),
   transaction: new TransactionAPI(),
   pageSetting: new PageSettingAPI(),
   registration: new RegistrationAPI(),
   unitCategory: new UnitCategoryAPI(),
+  mediaHashtag: new MediaHashtagAPI(),
+  mediaCategory: new MediaCategoryAPI(),
   ticketCategory: new TicketCategoryAPI(),
   sessionTemplate: new SessionTemplateAPI(),
+  contentCategory: new ContentCategoryAPI(),
   classroomRegistration: new ClassroomRegistrationAPI()
 }

+ 55 - 0
src/api/models/Content.js

@@ -0,0 +1,55 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { Content, ContentList } from 'src/models/Content.js'
+
+export default class ContentAPI extends APIRepository {
+  constructor() {
+    super('content', appApi)
+    this.APIAdresses = {
+      base: '/cma/contents',
+      byId: (id) => '/cma/contents/' + 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 Content(response.data) },
+      post: (response) => { return new Content(response.data) },
+      put: (response) => { return new Content(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new ContentList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

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

@@ -0,0 +1,57 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { ContentCategory, ContentCategoryList } from 'src/models/ContentCategory.js'
+
+export default class ContentCategoryAPI extends APIRepository {
+  constructor() {
+    super('content-category', appApi)
+    this.APIAdresses = {
+      base: '/cma/contents/categories',
+      byId: (id) => '/cma/contents/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 ContentCategory(response.data) },
+      post: (response) => { return new ContentCategory(response.data) },
+      put: (response) => { return new ContentCategory(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 ContentCategoryList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 55 - 0
src/api/models/Media.js

@@ -0,0 +1,55 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { Media, MediaList } from 'src/models/Media.js'
+
+export default class MediaAPI extends APIRepository {
+  constructor() {
+    super('media', appApi)
+    this.APIAdresses = {
+      base: '/cma/medias',
+      assignService: (id) => '/core/packages/' + id + '/assign'
+    }
+    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 Media(response.data) },
+      post: (response) => { return new Media(response.data) },
+      put: (response) => { return new Media(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new MediaList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 54 - 0
src/api/models/MediaCategory.js

@@ -0,0 +1,54 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { MediaCategory, MediaCategoryList } from 'src/models/MediaCategory.js'
+
+export default class MediaCategoryAPI extends APIRepository {
+  constructor() {
+    super('mediaCategory', appApi)
+    this.APIAdresses = {
+      base: '/cma/categories'
+    }
+    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 MediaCategory(response.data) },
+      post: (response) => { return new MediaCategory(response.data) },
+      put: (response) => { return new MediaCategory(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new MediaCategoryList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 54 - 0
src/api/models/MediaHashtag.js

@@ -0,0 +1,54 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { MediaHashtag, MediaHashtagList } from 'src/models/MediaHashtag.js'
+
+export default class MediaHashtagAPI extends APIRepository {
+  constructor() {
+    super('mediaHashtag', appApi)
+    this.APIAdresses = {
+      base: '/cma/hashtags'
+    }
+    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 MediaHashtag(response.data) },
+      post: (response) => { return new MediaHashtag(response.data) },
+      put: (response) => { return new MediaHashtag(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new MediaHashtagList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 10 - 15
src/api/models/PageSetting.js

@@ -5,10 +5,9 @@ import { PageSetting, PageSettingList } from 'src/models/PageSetting.js'
 export default class PageSettingAPI extends APIRepository {
   constructor() {
     super('page-setting', appApi)
-    this.serviceId = 1
     this.APIAdresses = {
-      base: '/admin/setting',
-      getWithKey: (key) => '/setting/' + key,
+      base: '/cma/settings',
+      getWithKey: (key) => '/cma/settings/key/' + key,
       updateWithKey: (key) => '/admin/setting/' + key
     }
     this.CacheList = {
@@ -23,7 +22,6 @@ export default class PageSettingAPI extends APIRepository {
       api: this.api,
       request: this.APIAdresses.base,
       cacheKey: this.CacheList.base,
-      data: { service_id: this.serviceId },
       ...(cache !== undefined && { cache }),
       resolveCallback: (response) => {
         return {
@@ -50,7 +48,6 @@ export default class PageSettingAPI extends APIRepository {
     const formData = new FormData()
     formData.append('key', data.key)
     formData.append('value', data.value)
-    formData.append('service_id', this.serviceId)
     return this.sendRequest({
       apiMethod: 'post',
       api: this.api,
@@ -68,17 +65,16 @@ export default class PageSettingAPI extends APIRepository {
   update (data) {
     const settingKey = encodeURI(data.key)
     const settingValue = data.value
-    const formData = new FormData()
-    formData.append('value', settingValue)
-    formData.append('service_id', this.serviceId)
-    formData.append('_method', 'PUT')
     return this.sendRequest({
       apiMethod: 'post',
       api: this.api,
-      request: this.APIAdresses.updateWithKey(settingKey),
-      data: formData,
+      request: this.APIAdresses.base,
+      data: {
+        key: settingKey,
+        value: settingValue
+      },
       resolveCallback: (response) => {
-        return response.data.data
+        return response
       },
       rejectCallback: (error) => {
         return error
@@ -86,16 +82,15 @@ export default class PageSettingAPI extends APIRepository {
     })
   }
 
-  get (data, cache = { TTL: 30000 }) {
+  get (data, cache = { TTL: 1000 }) {
     return this.sendRequest({
       apiMethod: 'get',
       api: this.api,
       request: this.APIAdresses.getWithKey(encodeURI(data)),
       cacheKey: this.CacheList.getWithKey(encodeURI(data)),
-      data: { service_id: this.serviceId },
       ...(cache !== undefined && { cache }),
       resolveCallback: (response) => {
-        return new PageSetting(response.data.data)
+        return new PageSetting(response.data)
       },
       rejectCallback: (error) => {
         return error

+ 2 - 0
src/boot/registerQPageBuilder.js

@@ -4,6 +4,7 @@ import widgetList from '../components/Widgets/widget.json'
 import QPageBuilder from 'quasar-ui-q-page-builder/src/components/QPageBuilder.vue'
 import PageBuilderComponents from '../components/Widgets/PageBuilderComponents.js'
 import PageBuilderOptionPanels from '../components/Widgets/PageBuilderOptionPanels.js'
+import ImageUploader from 'src/components/ImageUploader.vue'
 
 const widgets = []
 function extractWidgets (json) {
@@ -25,6 +26,7 @@ export default boot(function ({ app }) {
 
   app.config.globalProperties.$q.$pageBuilderWidgetComponents = PageBuilderComponents
   app.config.globalProperties.$q.$pageBuilderWidgetOptionPanels = PageBuilderOptionPanels
+  app.config.globalProperties.$q.$pageBuilderWidgetOptionPanelImageUploader = ImageUploader
   // app.component('q-page-builder', VuePlugin.QPageBuilder)
   app.component('q-page-builder', QPageBuilder)
 })

+ 135 - 0
src/components/FormBuilderCustumComponents/ContentCategorySelector.vue

@@ -0,0 +1,135 @@
+<template>
+  <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="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>
+</template>
+
+<script>
+import { APIGateway } from 'src/api/APIGateway.js'
+import { ContentCategoryList } from 'src/models/ContentCategory.js'
+
+export default {
+  name: 'ContentCategorySelector',
+  props: {
+    value: {
+      default: () => [],
+      type: [Array, String, Number, Boolean]
+    },
+    disable: {
+      default: false,
+      type: Boolean
+    }
+  },
+  emits: ['update:value'],
+  data () {
+    return {
+      mainCategory: null,
+      subCategory: null,
+      bakhshCategory: null,
+      mainCategoryOptions: [],
+      subCategoryOptions: [],
+      bakhshCategoryOptions: [],
+      contentCategories: new ContentCategoryList()
+    }
+  },
+  watch: {
+    value () {
+      this.inputData = this.value
+    },
+    mainCategory (newValue) {
+      this.subCategory = null
+      this.subCategoryOptions = []
+      this.subCategoryOptions = newValue.item.children.map(item => {
+        return {
+          item,
+          label: item.title,
+          value: item.id
+        }
+      })
+    },
+    subCategory (newValue) {
+      this.bakhshCategory = null
+      this.bakhshCategoryOptions = []
+      this.bakhshCategoryOptions = newValue.item.children.map(item => {
+        return {
+          item,
+          label: item.title,
+          value: item.id
+        }
+      })
+    },
+    bakhshCategory (newValue) {
+      const categoryId = newValue ? newValue.value : null
+      this.$emit('update:value', categoryId)
+    }
+  },
+  created () {
+    this.inputData = this.value
+  },
+  mounted() {
+    this.getCategories()
+  },
+  methods: {
+    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 => {
+            return {
+              item,
+              label: item.title,
+              value: item.id
+            }
+          })
+
+          if (this.value?.parent?.parent?.id && this.value.id) {
+            this.$nextTick(() => {
+              this.mainCategory = this.mainCategoryOptions.find(item => item.value === this.value.parent.parent.id)
+              this.$nextTick(() => {
+                this.subCategory = this.subCategoryOptions.find(item => item.value === this.value.parent.id)
+                this.$nextTick(() => {
+                  this.bakhshCategory = this.bakhshCategoryOptions.find(item => item.value === this.value.id)
+                  this.$emit('update:value', this.value.id)
+                })
+              })
+            })
+          }
+        })
+        .catch(() => {
+          this.contentCategories.laoding = false
+        })
+    }
+  }
+
+}
+</script>
+
+<style lang="scss" scoped>
+.ContentCategorySelector {
+}
+</style>

+ 124 - 0
src/components/FormBuilderCustumComponents/ContentMedias.vue

@@ -0,0 +1,124 @@
+<template>
+  <q-card class="ContentMedias">
+    <q-card-section>
+      <q-list bordered
+              separator
+              class="q-mb-md">
+        <q-item class="media-item">
+          <q-item-label>
+            لیست چند رسانه ای های تعریف شده
+          </q-item-label>
+        </q-item>
+        <q-item v-for="(media, mediaIndex) in medias.list"
+                :key="media.id"
+                class="media-item">
+          <q-item-section>
+            <q-item-label>
+              <q-btn icon="isax:trash"
+                     round
+                     color="red"
+                     @click="removeMedia(mediaIndex)" />
+              {{ media.title }}
+            </q-item-label>
+            <q-item-label caption>{{ media.type_info?.label }}</q-item-label>
+          </q-item-section>
+        </q-item>
+      </q-list>
+      <entity-create ref="entityCreate"
+                     v-model:value="inputs"
+                     title="تعریف چند رسانه ای"
+                     :api="api"
+                     :entity-id-key="entityIdKey"
+                     :entity-param-key="entityParamKey"
+                     :show-close-button="false"
+                     :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>
+    </q-card-section>
+  </q-card>
+</template>
+
+<script>
+import { EntityCreate } from 'quasar-crud'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { Media, MediaList } from 'src/models/Media.js'
+
+export default {
+  name: 'ContentMedias',
+  components: { EntityCreate },
+  props: {
+    value: {
+      default: () => [],
+      type: [Array, String, Number, Boolean]
+    },
+    disable: {
+      default: false,
+      type: Boolean
+    }
+  },
+  emits: ['update:value'],
+  data () {
+    return {
+      medias: new MediaList(),
+      api: APIGateway.media.APIAdresses.base,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-md-4 col-12' },
+        { type: 'select', name: 'type', responseKey: 'type', label: 'نوع', placeholder: ' ', options: (new Media()).typeEnums, col: 'col-md-4 col-12' },
+        { type: 'file', name: 'file', responseKey: 'file', label: 'فایل', placeholder: ' ', col: 'col-md-4 col-12' },
+        { type: 'input', name: 'url', responseKey: 'url', label: 'url', placeholder: ' ', col: 'col-12' },
+        { type: 'input', name: 'iframe_code', responseKey: 'iframe_code', label: 'iframe_code', placeholder: ' ', col: 'col-12' },
+        { type: 'input', name: 'script_code', responseKey: 'script_code', label: 'script_code', placeholder: ' ', col: 'col-12' },
+        { type: 'hidden', name: 'source_type', responseKey: 'source_type', value: 'CONTENT' }
+      ],
+      entityCreateLoading: false
+    }
+  },
+  watch: {
+    value () {
+      this.inputData = this.value
+      if (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))
+      }
+    }
+  },
+  created () {
+    this.inputData = this.value
+  },
+  methods: {
+    removeMedia (mediaIndex) {
+      this.medias.list.splice(mediaIndex, 1)
+      this.$emit('update:value', this.medias.list.map(item => item.id))
+    },
+    entityCreate () {
+      this.entityCreateLoading = true
+      this.$refs.entityCreate.createEntity(false)
+        .then(response => {
+          this.medias.list.push(new Media(response.data))
+          this.entityCreateLoading = false
+          this.$emit('update:value', this.medias.list.map(item => item.id))
+        })
+        .catch(() => {
+          this.entityCreateLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.ContentMedias {
+  :deep(.media-item) {
+    display: block;
+  }
+}
+</style>

+ 0 - 84
src/components/FormBuilderCustumComponents/Map/ActivityType.vue

@@ -1,84 +0,0 @@
-<template>
-  <div>
-    <q-select v-model="inputData.id"
-              :options="activityTypeOptions"
-              option-value="id"
-              emit-value
-              map-options
-              label="انتخاب نوع فعالیت :"
-              @update:model-value="onChangeActivityType" />
-    <div v-show="inputData.name !== 'noAction'"
-         v-if="inputData.data">
-      <q-input v-model="inputData.data.link"
-               label="لینک مد نظر خود را وارد کنید:" />
-      <q-select v-model="inputData.data.target"
-                option-value="value"
-                emit-value
-                map-options
-                :options="LinkStateOptions" />
-    </div>
-  </div>
-</template>
-
-<script>
-// import { inputMixin } from 'quasar-form-builder'
-import inputMixin from 'quasar-form-builder/src/mixins/inputMixin.js'
-
-export default {
-  name: 'ActivityType',
-  mixins: [inputMixin],
-  props: {
-    value: {
-      default: null
-    }
-  },
-  data() {
-    return {
-      activityTypeOptions: [
-        {
-          id: 0,
-          label: 'بدون فعالیت',
-          name: 'noAction'
-        },
-        {
-          id: 1,
-          label: 'لینک',
-          name: 'link'
-
-        },
-        {
-          id: 2,
-          label: 'زوم',
-          name: 'zoom'
-
-        }
-      ],
-      LinkStateOptions: [
-        {
-          label: 'باز شدن لینک در صفحه جدید',
-          value: '_blank'
-        },
-        {
-          label: 'باز شدن لینک در همین صفحه',
-          value: '_self'
-        }
-
-      ]
-    }
-  },
-  created () {
-  },
-  methods: {
-    onChangeActivityType () {
-      const name = this.activityTypeOptions.find(item => item.id === this.inputData.id).name
-      const data = { id: this.inputData.id, name, data: {} }
-      this.change(data)
-    }
-
-  }
-}
-</script>
-
-<style scoped>
-
-</style>

+ 0 - 79
src/components/FormBuilderCustumComponents/Map/ItemEntity.vue

@@ -1,79 +0,0 @@
-<template>
-  <q-option-group v-model="inputData.entity_type"
-                  :options="entityOptions"
-                  color="primary"
-                  type="radio"
-                  dense
-                  inline
-                  @update:model-value="onChangeLineType" />
-  <div v-if="inputData.entity_type !== 'nothing' && inputData.entity_type">
-    <q-input v-model="inputData.altNames"
-             label="کد موجودیت را وارد کنید:" />
-
-  </div>
-</template>
-
-<script>
-import { inputMixin } from 'quasar-form-builder'
-export default {
-  name: 'ItemEntity',
-  mixins: [inputMixin],
-  props: {
-    value: {
-      default: null
-    }
-  },
-  data() {
-    return {
-      entityOptions: [
-        {
-          entity_id: 0,
-          entity_type: 'nothing',
-          value: 'nothing',
-          display_name: 'هیچ کدام',
-          label: 'هیچ کدام',
-          altNames: []
-        },
-        {
-          entity_id: 1,
-          entity_type: 'product',
-          value: 'product',
-          display_name: 'محصول',
-          label: 'محصول',
-          altNames: []
-        },
-        {
-          entity_id: 2,
-          entity_type: 'set',
-          value: 'set',
-          display_name: 'دسته محتوا',
-          label: 'دسته محتوا',
-          altNames: []
-        },
-        {
-          entity_id: 3,
-          entity_type: 'content',
-          value: 'content',
-          display_name: 'محتوا',
-          label: 'محتوا',
-          altNames: []
-        }
-      ]
-    }
-  },
-  methods: {
-    onChangeLineType () {
-      const name = this.entityOptions.find(item => item.entity_type === this.inputData.entity_type).display_name
-      const id = this.entityOptions.find(item => item.entity_type === this.inputData.entity_type).entity_id
-      const data = { entity_id: id, entity_type: this.inputData.entity_type, display_name: name, altNames: [] }
-      this.change(data)
-      // this.setLineFlow()
-    }
-  }
-
-}
-</script>
-
-<style scoped>
-
-</style>

+ 0 - 93
src/components/FormBuilderCustumComponents/Map/LineType.vue

@@ -1,93 +0,0 @@
-<template>
-  <div>
-    <div v-if="label">
-      {{ label }}
-    </div>
-
-    <q-option-group v-model="inputData.dir"
-                    :options="lineTypeOptions"
-                    color="primary"
-                    type="radio"
-                    dense
-                    inline
-                    @update:model-value="onChangeLineType" />
-  </div>
-  <div v-show="inputData.dir !=='fixed'">
-    <div v-if="inputData.style">
-      <q-badge color="secondary">
-        سرعت حرکت : از 0 تا 100 ( {{inputData.style['animation-duration'] }} )
-      </q-badge>
-      <q-slider v-model="inputData.style['animation-duration']"
-                :min="0"
-                :max="100"
-                :disable="disable"
-                @input="onChangeDuration" />
-    </div>
-  </div>
-</template>
-
-<script>
-import { inputMixin } from 'quasar-form-builder'
-
-export default {
-  name: 'LineType',
-  mixins: [inputMixin],
-  props: {
-    value: {
-      default: []
-    },
-    label: {
-      default: '',
-      type: String
-    }
-  },
-  data () {
-    return {
-      lineTypeOptions: [
-        {
-          id: 0,
-          label: 'بدون حرکت',
-          value: 'fixed'
-        }, {
-          id: 1,
-          label: 'متحرک',
-          value: 'flowing'
-        }, {
-          id: 2,
-          label: ' حرکت معکوس',
-          value: 'reverse'
-        }
-      ]
-    }
-  },
-  created () {
-  },
-  methods: {
-    onChangeDuration () {
-      // this.setLineFlow()
-    },
-    onChangeLineType () {
-      const data = { dir: this.inputData.dir, style: { 'animation-duration': 17 } }
-      this.change(data)
-      // this.setLineFlow()
-    }
-    // prepareDefaultInputData () {
-    //   if (this.inputData && this.inputData.style && typeof this.inputData.style['animation-duration'] !== 'undefined') {
-    //     return
-    //   }
-    //   this.inputData.style = {}
-    //   this.inputData.style['animation-duration'] = 17
-    //   console.log(this.inputData)
-    // },
-
-    // setLineFlow() {
-    //   this.prepareDefaultInputData()
-    //   this.change(this.inputData)
-    // }
-  }
-}
-</script>
-
-<style scoped>
-
-</style>

+ 86 - 0
src/components/ImageUploader.vue

@@ -0,0 +1,86 @@
+<template>
+  <q-card>
+    <q-card-section>
+      <div>
+        ({{ value || file }})
+      </div>
+      <entity-create ref="entityCreate"
+                     v-model:value="inputs"
+                     title="آپلود تصویر"
+                     :api="api"
+                     :default-layout="false"
+                     :entity-id-key-in-response="entityIdKeyInResponse"
+                     :show-route-param-key="showRouteParamKey"
+                     :index-route-name="indexRouteName"
+                     :show-route-name="showRouteName"
+                     :after-send-data="afterSendData" />
+      <div class="action">
+        <div class="row q-mt-lg justify-end">
+          <div class="col-md-4 col-12">
+            <q-btn color="primary"
+                   class="full-width"
+                   :loading="entityLoading"
+                   @click="create">
+              آپلود
+            </q-btn>
+          </div>
+        </div>
+      </div>
+    </q-card-section>
+  </q-card>
+</template>
+
+<script>
+import { EntityCreate } from 'quasar-crud'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'ImageUploader',
+  components: { EntityCreate },
+  props: {
+    value: {
+      type: String,
+      default: null
+    },
+    file: {
+      type: String,
+      default: null
+    }
+  },
+  data: () => {
+    return {
+      entityLoading: false,
+      api: APIGateway.media.APIAdresses.base,
+      entityIdKeyInResponse: 'id',
+      showRouteParamKey: 'id',
+      showRouteName: 'AdminPanel.Service.Show',
+      indexRouteName: 'AdminPanel.Service.List',
+      inputs: [
+        { type: 'file', name: 'file', responseKey: 'file', label: 'فایل تصویر', col: 'col-md-6 col-12' },
+        { type: 'hidden', name: 'source_type', value: 'SETTING' },
+        { type: 'hidden', name: 'type', value: 'VIDEO' }
+      ]
+    }
+  },
+  methods: {
+    create() {
+      this.entityLoading = true
+      this.$refs.entityCreate.createEntity(false)
+        .then(() => {
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.entityLoading = false
+        })
+    },
+    afterSendData (response) {
+      this.$emit('update:file', response.data.file)
+      this.$emit('update:value', "url('" + response.data.file + "')")
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 0 - 3
src/components/Template/FloatingActionButton/FloatingActionButton.vue

@@ -211,9 +211,6 @@ export default {
   },
   methods: {
     truncateString(string, length) {
-      if (!string) {
-        return ''
-      }
       return string.length > length ? string.slice(0, length) + '...' : string
     },
     importPageBuilderConfigs () {

+ 37 - 3
src/components/Template/SideBard/AdminPanelSideBar.vue

@@ -21,7 +21,7 @@ export default {
           active: false
         },
         {
-          title: 'سامانه آموزشی',
+          title: 'مدیریت آموزش',
           icon: 'isax:book',
           routeName: null,
           show: true,
@@ -29,16 +29,50 @@ export default {
           active: false,
           children: [
             {
-              title: 'دسته بندی ها',
+              title: 'درس ها و چارت آموزشی',
               routeName: 'Admin.Category.Index',
               show: true,
               active: false
             },
             {
-              title: 'دوره های آموزشی',
+              title: 'کلاس های آموزشی',
               routeName: 'Admin.Classroom.Index',
               show: true,
               active: false
+            },
+            {
+              title: 'درخواست ها',
+              routeName: 'Admin.Classroom.Ticket',
+              show: true,
+              active: false
+            }
+          ]
+        },
+        {
+          title: 'محتوا',
+          icon: 'isax:video',
+          routeName: null,
+          show: true,
+          open: true,
+          active: false,
+          children: [
+            {
+              title: 'دسته بندی ها',
+              routeName: 'Admin.ContentCategory.List',
+              show: true,
+              active: false
+            },
+            {
+              title: 'لیست محتوا ها',
+              routeName: 'Admin.Content.List',
+              show: true,
+              active: false
+            },
+            {
+              title: 'درخواست ها',
+              routeName: 'Admin.Content.Ticket',
+              show: true,
+              active: false
             }
           ]
         }

+ 104 - 0
src/components/Widgets/Content/AdminContentCreate/AdminContentCreate.vue

@@ -0,0 +1,104 @@
+<template>
+  <div class="AdminContentCreate"
+       :style="localOptions.style">
+    <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 'src/components/Control/btn.vue'
+import ContentMedias from 'src/components/FormBuilderCustumComponents/ContentMedias.vue'
+import ContentCategorySelector from 'src/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-md-12 col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', col: 'col-md-12 col-12' },
+        { type: ContentMediasComp, name: 'medias', responseKey: 'medias', col: 'col-md-12 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/Content/AdminContentCreate/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>

+ 106 - 0
src/components/Widgets/Content/AdminContentList/AdminContentList.vue

@@ -0,0 +1,106 @@
+<template>
+  <div class="AdminContentList"
+       :style="localOptions.style">
+    <div class="more-action">
+      <q-btn label="محتوای جدید"
+             color="primary"
+             :to="{name: 'Admin.Content.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 === 'action'">
+          <q-btn color="primary"
+                 :to="{name: 'Admin.Content.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: 'AdminContentList',
+  components: { EntityIndex },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      api: APIGateway.content.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">
+.AdminContentList {
+  .more-action {
+    display: flex;
+    flex-flow: row;
+    justify-content: flex-end;
+    margin-bottom: 10px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Content/AdminContentList/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>

+ 127 - 0
src/components/Widgets/Content/AdminContentShow/AdminContentShow.vue

@@ -0,0 +1,127 @@
+<template>
+  <div class="AdminContentShow"
+       :style="localOptions.style">
+    <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 'src/components/Control/btn.vue'
+import ContentMedias from 'src/components/FormBuilderCustumComponents/ContentMedias.vue'
+import ContentCategorySelector from 'src/components/FormBuilderCustumComponents/ContentCategorySelector.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const ContentMediasComp = shallowRef(ContentMedias)
+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: 'category', responseKey: 'category_info', col: 'col-md-12 col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', col: 'col-md-12 col-12' },
+        { type: ContentMediasComp, name: 'medias', responseKey: 'medias_info', col: 'col-md-12 col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ویرایش محتوا', placeholder: ' ', atClick: () => {}, col: 'col-md-6' }
+      ]
+    }
+  },
+  computed: {
+    repliesInfo () {
+      return this.inputs.find(input => input.name === 'replies_info').value
+    }
+  },
+  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/Content/AdminContentShow/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/Content/ContentList/ContentList.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/Content/ContentList/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/Content/ContentShow/ContentShow.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: 'AdminPanel.Ticket.Show',
+      indexRouteName: 'AdminPanel.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>

+ 21 - 0
src/components/Widgets/Content/ContentShow/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>

+ 113 - 0
src/components/Widgets/ContentCategory/AdminContentCategoryCreate/AdminContentCategoryCreate.vue

@@ -0,0 +1,113 @@
+<template>
+  <div class="AdminContentCategoryCreate"
+       :style="localOptions.style">
+    <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 'src/components/Control/btn.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+
+export default {
+  name: 'AdminContentCategoryCreate',
+  components: { EntityCreate },
+  mixins: [mixinWidget],
+  props: {
+    title: {
+      type: String,
+      default: 'ایجاد دسته بندی اصلی'
+    },
+    parent: {
+      type: Number,
+      default: null
+    }
+  },
+  data: () => {
+    return {
+      mounted: false,
+      entityLoading: false,
+      api: APIGateway.contentCategory.APIAdresses.base,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.ContentCategory.Show',
+      indexRouteName: 'Admin.ContentCategory.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: null },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'تایید', placeholder: ' ', atClick: () => {}, col: 'col-12' }
+      ]
+    }
+  },
+  created() {
+    FormBuilderAssist.setAttributeByName(this.inputs, 'parent', 'value', this.parent)
+  },
+  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() {
+      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">
+.AdminContentCategoryCreate {
+  :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/ContentCategory/AdminContentCategoryCreate/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>

+ 120 - 0
src/components/Widgets/ContentCategory/AdminContentCategoryList/AdminContentCategoryList.vue

@@ -0,0 +1,120 @@
+<template>
+  <div class="AdminContentCategoryList"
+       :style="localOptions.style">
+    <div v-if="!parent"
+         class="more-action">
+      <q-btn label="دسته محتوای جدید"
+             color="primary"
+             :to="{name: 'Admin.ContentCategory.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 === 'action'">
+          <q-btn color="primary"
+                 :to="{name: 'Admin.ContentCategory.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: 'AdminContentCategoryList',
+  components: { EntityIndex },
+  mixins: [mixinWidget],
+  props: {
+    title: {
+      type: String,
+      default: 'لیست دسته بندی های کلی'
+    },
+    parent: {
+      type: Number,
+      default: null
+    }
+  },
+  data: () => {
+    return {
+      api: APIGateway.contentCategory.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: '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">
+.AdminContentCategoryList {
+  .more-action {
+    display: flex;
+    flex-flow: row;
+    justify-content: flex-end;
+    margin-bottom: 10px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/ContentCategory/AdminContentCategoryList/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>

+ 117 - 0
src/components/Widgets/ContentCategory/AdminContentCategoryShow/AdminContentCategoryShow.vue

@@ -0,0 +1,117 @@
+<template>
+  <div class="AdminContentCategoryShow"
+       :style="localOptions.style">
+    <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="contentCategory.id"
+             class="q-mt-lg">
+          <admin-content-category-list :key="listKey"
+                                       title="لیست دسته بندی های جزیی"
+                                       :parent="contentCategory.id" />
+          <q-separator class="q-my-lg" />
+          <admin-content-category-create :key="listKey"
+                                         title="ایجاد دسته بندی جزیی"
+                                         :parent="contentCategory.id"
+                                         @onCreated="onCreated" />
+        </div>
+      </template>
+    </entity-edit>
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityEdit, EntityCreate } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import BtnControl from 'src/components/Control/btn.vue'
+import { ContentCategory } from 'src/models/ContentCategory.js'
+import AdminContentCategoryList from 'src/components/Widgets/ContentCategory/AdminContentCategoryList/AdminContentCategoryList.vue'
+import AdminContentCategoryCreate from 'src/components/Widgets/ContentCategory/AdminContentCategoryCreate/AdminContentCategoryCreate.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+
+export default {
+  name: 'AdminContentCategoryShow',
+  components: {
+    AdminContentCategoryList,
+    AdminContentCategoryCreate,
+    EntityEdit
+  },
+  mixins: [mixinWidget, EntityCreate],
+  data: () => {
+    return {
+      listKey: Date.now(),
+      replyText: null,
+      mounted: false,
+      entityLoading: true,
+      contentCategory: new ContentCategory(),
+      api: null,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'AdminPanel.Ticket.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: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', placeholder: ' ', col: 'col-md-12 col-12' },
+        { type: 'hidden', name: 'parent', responseKey: 'parent', value: null },
+        { type: 'hidden', name: 'id', responseKey: 'id', value: null },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'تایید', placeholder: ' ', atClick: () => {}, col: 'col-12' }
+      ]
+    }
+  },
+  computed: {
+    repliesInfo () {
+      return this.inputs.find(input => input.name === 'replies_info').value
+    }
+  },
+  created() {
+    this.contentCategory.loading = true
+    this.api = APIGateway.contentCategory.APIAdresses.byId(this.$route.params.id)
+  },
+  mounted() {
+    this.mounted = true
+  },
+  methods: {
+    afterLoadInputData (data) {
+      this.contentCategory = new ContentCategory(data)
+      this.contentCategory.loading = false
+      this.entityLoading = false
+    },
+    edit() {
+      this.entityLoading = true
+      this.$refs.entityEdit.editEntity()
+        .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">
+.AdminContentCategoryShow {
+}
+</style>

+ 21 - 0
src/components/Widgets/ContentCategory/AdminContentCategoryShow/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 - 50
src/components/Widgets/Ticket/AdminTicketList/AdminTicketList.vue

@@ -1,36 +1,28 @@
 <template>
   <div class="AdminTicketList"
        :style="localOptions.style">
-    <div class="title">
-      پیام‌های دریافتی
-    </div>
-    <q-card class="list"
-            flat>
-      <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"
-                    :default-layout="false">
-        <template #entity-index-table-cell="{inputData}">
-          <template v-if="inputData.col.name === 'action'">
-            <q-btn flat
-                   color="primary"
-                   :to="{name: 'AdminPanel.Ticket.Show', params: {id: inputData.props.row.id}}">
-              مشاهده جزییات
-            </q-btn>
-          </template>
-          <template v-else>
-            {{ inputData.col.value }}
-          </template>
+    <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 === 'action'">
+          <q-btn color="primary"
+                 :to="{name: 'AdminPanel.Ticket.Show', params: {id: inputData.props.row.id}}">
+            مشاهده جزییات
+          </q-btn>
         </template>
-      </entity-index>
-    </q-card>
+        <template v-else>
+          {{ inputData.col.value }}
+        </template>
+      </template>
+    </entity-index>
   </div>
 </template>
 
@@ -99,26 +91,5 @@ export default {
 
 <style scoped lang="scss">
 .AdminTicketList {
-  .title {
-    font-style: normal;
-    font-weight: 700;
-    font-size: 24px;
-    line-height: 140%;
-    color: #424242;
-    margin-bottom: 27px;
-  }
-  :deep(.list) {
-    .quasar-crud-index-table {
-      padding: 0;
-      .q-table__container {
-        background-color: transparent;
-        box-shadow: none;
-        border: none;
-        .q-table__top {
-          display: none;
-        }
-      }
-    }
-  }
 }
 </style>

+ 19 - 0
src/models/Celebrity.js

@@ -0,0 +1,19 @@
+import { Collection, Model } from 'js-abstract-model'
+
+class Celebrity extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      { key: 'name' },
+      { key: 'thumbnail' }
+    ])
+  }
+}
+
+class CelebrityList extends Collection {
+  model () {
+    return Celebrity
+  }
+}
+
+export { Celebrity, CelebrityList }

+ 50 - 0
src/models/Content.js

@@ -0,0 +1,50 @@
+import { Collection, Model } from 'js-abstract-model'
+import { CelebrityList } from 'src/models/Celebrity.js'
+import { ContentHashtagList } from 'src/models/ContentHashtag.js'
+import { ContentCategoryList } from 'src/models/ContentCategory.js'
+
+class Content extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      { key: 'title' },
+      { key: 'description' },
+      { key: 'summary' },
+      { key: 'thumbnail' },
+      {
+        key: 'is_free',
+        defalut: false
+      },
+      {
+        key: 'medias',
+        defalut: []
+      },
+      { key: 'category' },
+      {
+        key: 'category_info',
+        relatedModel: ContentCategoryList
+      },
+      {
+        key: 'celebrities',
+        relatedModel: CelebrityList
+      },
+      { key: 'hashtags' },
+      {
+        key: 'hashtags_info',
+        relatedModel: ContentHashtagList
+      }
+    ])
+
+    if (!this.thumbnail) {
+      this.thumbnail = '/img/custom/Mask_group2.png'
+    }
+  }
+}
+
+class ContentList extends Collection {
+  model () {
+    return Content
+  }
+}
+
+export { Content, ContentList }

+ 23 - 0
src/models/ContentCategory.js

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

+ 19 - 0
src/models/ContentHashtag.js

@@ -0,0 +1,19 @@
+import { Collection, Model } from 'js-abstract-model'
+
+class ContentHashtag extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      { key: 'title' },
+      { key: 'order' }
+    ])
+  }
+}
+
+class ContentHashtagList extends Collection {
+  model () {
+    return ContentHashtag
+  }
+}
+
+export { ContentHashtag, ContentHashtagList }

+ 72 - 0
src/models/Media.js

@@ -0,0 +1,72 @@
+import { Collection, Model } from 'js-abstract-model'
+import { CelebrityList } from 'src/models/Celebrity.js'
+import { MediaHashtagList } from 'src/models/MediaHashtag.js'
+import { MediaCategory } from 'src/models/MediaCategory.js'
+
+class Media extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      { key: 'title' },
+      { key: 'type' }, // VIDEO - TEXT - SOUND - IMAGE
+      { key: 'source_type' }, // CONTENT - SETTING
+      { key: 'url' },
+      { key: 'iframe_code' },
+      { key: 'script_code' },
+      { key: 'is_live_streaming' },
+      { key: 'category' },
+      {
+        key: 'category_info',
+        relatedModel: MediaCategory
+      },
+      { key: 'description' },
+      {
+        key: 'celebrities',
+        relatedModel: CelebrityList
+      },
+      { key: 'hashtags' },
+      {
+        key: 'hashtags_info',
+        relatedModel: MediaHashtagList
+      }
+    ])
+
+    this.typeEnums = [
+      {
+        label: 'ویدیو',
+        value: 'VIDEO'
+      },
+      {
+        label: 'متن',
+        value: 'TEXT'
+      },
+      {
+        label: 'صوت',
+        value: 'SOUND'
+      },
+      {
+        label: 'تصویر',
+        value: 'IMAGE'
+      }
+    ]
+
+    this.loadTypeInfo()
+  }
+
+  loadTypeInfo () {
+    const target = this.typeEnums.find(type => type.value === this.type)
+    if (!target) {
+      this.type_info = null
+    } else {
+      this.type_info = target
+    }
+  }
+}
+
+class MediaList extends Collection {
+  model () {
+    return Media
+  }
+}
+
+export { Media, MediaList }

+ 23 - 0
src/models/MediaCategory.js

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

+ 19 - 0
src/models/MediaHashtag.js

@@ -0,0 +1,19 @@
+import { Collection, Model } from 'js-abstract-model'
+
+class MediaHashtag extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      { key: 'title' },
+      { key: 'order' }
+    ])
+  }
+}
+
+class MediaHashtagList extends Collection {
+  model () {
+    return MediaHashtag
+  }
+}
+
+export { MediaHashtag, MediaHashtagList }

+ 23 - 0
src/pages/Admin/Classroom/Ticket.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: 'AdminPanel.Package.List',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminTicketList' }, { name: 'AdminTicketCreate', options: { style: { marginTop: '32px' } } }], options: { style: { paddingTop: '' } } }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '30px', style: { maxWidth: '1200px', width: '1200px' } } }] }, options: { fullHeight: false, backgrounds: { xs: { size: null, color: null, image: null, repeat: null, position: null, attachment: null }, sm: { size: null, color: null, image: null, repeat: null, position: null, attachment: null }, md: { size: null, color: null, image: null, repeat: null, position: null, attachment: null }, lg: { size: null, color: null, image: null, repeat: null, position: null, attachment: null }, xl: { size: null, color: null, image: null, repeat: null, position: null, attachment: null } }, verticalAlign: null, style: { minHeight: 'auto' } } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 23 - 0
src/pages/Admin/Content/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: 'AdminPanel.Package.Show',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminContentCreate' }], options: {} }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '30px', style: { maxWidth: '1200px', width: '1200px' } } }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 23 - 0
src/pages/Admin/Content/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: 'AdminPanel.Package.List',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminContentList' }], options: {} }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '30px', style: { maxWidth: '1200px', width: '1200px' } } }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 23 - 0
src/pages/Admin/Content/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: 'AdminPanel.Package.Show',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminContentShow' }], options: { style: { paddingTop: '' } } }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '30px', 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/Content/Ticket.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: 'AdminPanel.Package.List',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminTicketList' }, { name: 'AdminTicketCreate', options: { style: { marginTop: '32px' } } }], options: { style: { paddingTop: '' } } }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '30px', style: { maxWidth: '1200px', width: '1200px' } } }] }, options: { fullHeight: false, backgrounds: { xs: { size: null, color: null, image: null, repeat: null, position: null, attachment: null }, sm: { size: null, color: null, image: null, repeat: null, position: null, attachment: null }, md: { size: null, color: null, image: null, repeat: null, position: null, attachment: null }, lg: { size: null, color: null, image: null, repeat: null, position: null, attachment: null }, xl: { size: null, color: null, image: null, repeat: null, position: null, attachment: null } }, verticalAlign: null, style: { minHeight: 'auto' } } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 23 - 0
src/pages/Admin/ContentCategory/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: 'AdminPanel.Package.Show',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminContentCategoryCreate' }], options: {} }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '30px', style: { maxWidth: '1200px', width: '1200px' } } }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 23 - 0
src/pages/Admin/ContentCategory/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: 'AdminPanel.Package.List',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminContentCategoryList' }], options: {} }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '30px', style: { maxWidth: '1200px', width: '1200px' } } }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 23 - 0
src/pages/Admin/ContentCategory/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: 'AdminPanel.Package.Show',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminContentCategoryShow' }], options: {} }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '30px', style: { maxWidth: '1200px', width: '1200px' } } }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 23 - 0
src/pages/Admin/Ticket/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: 'AdminPanel.Package.Show',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminBlogCreate', options: { style: { marginTop: '124px' } } }], options: {} }], options: {} }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 23 - 0
src/pages/Admin/Ticket/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: 'AdminPanel.Package.List',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminTicketList' }, { name: 'AdminTicketCreate', options: { style: { marginTop: '32px' } } }], options: { style: { paddingTop: '124px' } } }], options: {} }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 23 - 0
src/pages/Admin/Ticket/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: 'AdminPanel.Package.Show',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminTicketShow' }], options: { style: { paddingTop: '124px' } } }], options: {} }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 45 - 1
src/router/routes.js

@@ -264,6 +264,28 @@ const routes = [
               { name: 'Admin.SessionTemplate.Show', path: ':id', component: () => import('src/pages/Admin/SessionTemplate/show.vue') }
             ]
           },
+          {
+            path: 'ticket',
+            name: 'AdminPanel.Ticket',
+            component: () => import('layouts/bareLayout.vue'),
+            children: [
+              {
+                path: '',
+                name: 'AdminPanel.Ticket.List',
+                component: () => import('src/pages/Admin/Ticket/List.vue')
+              },
+              {
+                path: 'create',
+                name: 'AdminPanel.Ticket.Create',
+                component: () => import('src/pages/Admin/Ticket/Create.vue')
+              },
+              {
+                path: ':id',
+                name: 'AdminPanel.Ticket.Show',
+                component: () => import('src/pages/Admin/Ticket/Show.vue')
+              }
+            ]
+          },
           {
             name: 'Admin.Classroom',
             path: 'classrooms',
@@ -272,7 +294,29 @@ const routes = [
             children: [
               { name: 'Admin.Classroom.Index', path: '', component: () => import('src/pages/Admin/Classroom/index.vue') },
               { name: 'Admin.Classroom.Create', path: 'create', component: () => import('src/pages/Admin/Classroom/create.vue'), breadcrumbs: { title: 'ساخت دوره آموزشی' } },
-              { name: 'Admin.Classroom.Show', path: ':id', component: () => import('src/pages/Admin/Classroom/show.vue'), breadcrumbs: { title: 'مشاهده اطلاعات دوره آموزشی' } }
+              { name: 'Admin.Classroom.Show', path: ':id', component: () => import('src/pages/Admin/Classroom/show.vue'), breadcrumbs: { title: 'مشاهده اطلاعات دوره آموزشی' } },
+              { name: 'Admin.Classroom.Ticket', path: 'ticket', component: () => import('src/pages/Admin/Classroom/Ticket.vue'), breadcrumbs: { title: 'درخواست ها دوره های آموزشی' } }
+            ]
+          },
+          {
+            path: 'content',
+            name: 'Admin.Content',
+            component: () => import('layouts/bareLayout.vue'),
+            children: [
+              { path: '', name: 'Admin.Content.List', component: () => import('src/pages/Admin/Content/List.vue') },
+              { path: 'create', name: 'Admin.Content.Create', component: () => import('src/pages/Admin/Content/Create.vue') },
+              { path: ':id', name: 'Admin.Content.Show', component: () => import('src/pages/Admin/Content/Show.vue') },
+              { name: 'Admin.Content.Ticket', path: 'ticket', component: () => import('src/pages/Admin/Classroom/Ticket.vue'), breadcrumbs: { title: 'درخواست ها دوره های آموزشی' } }
+            ]
+          },
+          {
+            path: 'content-category',
+            name: 'Admin.ContentCategory',
+            component: () => import('layouts/bareLayout.vue'),
+            children: [
+              { path: '', name: 'Admin.ContentCategory.List', component: () => import('src/pages/Admin/ContentCategory/List.vue') },
+              { path: 'create', name: 'Admin.ContentCategory.Create', component: () => import('src/pages/Admin/ContentCategory/Create.vue') },
+              { path: ':id', name: 'Admin.ContentCategory.Show', component: () => import('src/pages/Admin/ContentCategory/Show.vue') }
             ]
           },
           {

+ 13 - 8
src/store/PageBuilder/actions.js

@@ -1,12 +1,16 @@
 import { Notify } from 'quasar'
-import { APIGateway } from 'src/api/APIGateway'
+import { appApi } from 'src/boot/axios.js'
+import API_ADDRESS from 'src/api/Addresses'
+
+const axios = appApi
 
 const actions = {
   editPageWidget: (context, data) => {
     return new Promise((resolve, reject) => {
-      APIGateway.pageSetting.update({ key: data.key, value: JSON.stringify(data.sections) })
-        .then(pageData => {
-          const parsedData = JSON.parse(pageData.value)
+      axios
+        .put(API_ADDRESS.pages.base, { key: data.key, value: JSON.stringify(data.sections) })
+        .then(r => {
+          const parsedData = JSON.parse(r.data.data.value)
           context.commit('updateCurrentSections', parsedData)
           context.commit('updateInitialSections', parsedData)
           Notify.create({
@@ -21,12 +25,13 @@ const actions = {
   },
   getPageWidget: (context, value) => {
     return new Promise((resolve, reject) => {
-      APIGateway.pageSetting.get(value)
-        .then(pageData => {
-          const parsedData = JSON.parse(pageData.value)
+      axios
+        .get(API_ADDRESS.pages.show(value))
+        .then(r => {
+          const parsedData = JSON.parse(r.data.data.value)
           context.commit('updateCurrentSections', parsedData)
           context.commit('updateInitialSections', parsedData)
-          resolve(pageData)
+          resolve(r)
         })
         .catch(e => {
           reject(e)

+ 1 - 1
src/store/PageBuilder/state.js

@@ -1,7 +1,7 @@
 export default function () {
   return {
     pageDataLoaded: false,
-    pageBuilderLoading: true,
+    pageBuilderLoading: false,
     pageBuilderEditable: false,
     initialSections: [],
     currentSections: []

+ 37 - 27
yarn.lock

@@ -1708,6 +1708,11 @@
   resolved "https://registry.yarnpkg.com/@quasar/extras/-/extras-1.16.4.tgz#34fc2517fffecb5c27e4cea1b69b62aaf890ff73"
   integrity sha512-q2kPTNHI5aprgE2yQfRIf6aud+qSXH3YTNmhcfRp/rENh7kRjoM+b5BBPxgHlO1si1ARddbmr+Fxu/L05hfXnQ==
 
+"@quasar/extras@^1.16.5":
+  version "1.16.5"
+  resolved "https://registry.yarnpkg.com/@quasar/extras/-/extras-1.16.5.tgz#77fd7b8e77e45a2798067a1e4aeafb39a95daf51"
+  integrity sha512-3VAJS9NECr1OSHX674+3YvEMIlHclr81aZrEkoBtVqr+sX4In22Up44toua+qNFsxnoATPqzpwKOJxA3iAF71Q==
+
 "@quasar/render-ssr-error@^1.0.1":
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/@quasar/render-ssr-error/-/render-ssr-error-1.0.2.tgz#92abb0d61cfdfbf51c2ec3cc2e4aadbf1c79f04f"
@@ -2756,10 +2761,10 @@ core-js@^2.4.0:
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
   integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
 
-core-js@^3.31.0:
-  version "3.31.0"
-  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.31.0.tgz#4471dd33e366c79d8c0977ed2d940821719db344"
-  integrity sha512-NIp2TQSGfR6ba5aalZD+ZQ1fSxGhDo/s1w0nx3RYzf2pnJxt7YynxFlFScP6eV7+GZsKO95NSjGxyJsU3DZgeQ==
+core-js@^3.31.1:
+  version "3.32.0"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.32.0.tgz#7643d353d899747ab1f8b03d2803b0312a0fb3b6"
+  integrity sha512-rd4rYZNlF3WuoYuRIDEmbR/ga9CeuWX9U05umAvgrrZoHY4Z++cp/xwPQMvUpBB4Ag6J8KfD80G0zwCyaSxDww==
 
 core-util-is@~1.0.0:
   version "1.0.3"
@@ -5394,35 +5399,35 @@ qs@6.11.0:
   dependencies:
     side-channel "^1.0.4"
 
-quasar-app-extension-q-page-builder@^0.1.49:
-  version "0.1.49"
-  resolved "https://registry.yarnpkg.com/quasar-app-extension-q-page-builder/-/quasar-app-extension-q-page-builder-0.1.49.tgz#0a4cb3fce33869cc4aa32dbdda72d99c65fef85b"
-  integrity sha512-so4AnnXMXrty4dbgjZfp3+N35KqCah2iLRog9/ldajSurlWh9XG+msr0L3NGgjA+MSdekbsfRU7/exnIA/c6FQ==
+quasar-app-extension-q-page-builder@^0.1.51:
+  version "0.1.51"
+  resolved "https://registry.yarnpkg.com/quasar-app-extension-q-page-builder/-/quasar-app-extension-q-page-builder-0.1.51.tgz#f6307e086243b522db7ae0eed6a6d41dd56f8aec"
+  integrity sha512-RYVS9/IMtR8f6zrZpt0xUo9qgFaYSAwRJtWPQ1WvWndYcPBbvtfeoeqFAexBBCS/dgNizZPlY95diCjNXj2aDw==
   dependencies:
-    quasar-ui-q-page-builder "^0.1.49"
+    quasar-ui-q-page-builder "^0.1.51"
 
-quasar-crud@^0.3.31:
-  version "0.3.31"
-  resolved "https://registry.yarnpkg.com/quasar-crud/-/quasar-crud-0.3.31.tgz#e60340022cad67b1550d3379ddd1940739a63fe7"
-  integrity sha512-AT+r6Ny78eNvkVXJZZVHvAW4sjt0U+QRV+IoYzuO7SH+mgJ4SOewSKTPBDC905Ktn71S+hiccn4wz/l3I8YIWA==
+quasar-crud@^0.3.42:
+  version "0.3.42"
+  resolved "https://registry.yarnpkg.com/quasar-crud/-/quasar-crud-0.3.42.tgz#593a1bb3e228e7a5864a5cc687e5c7cb776d5500"
+  integrity sha512-r8HI+Pc46Saemfdond2tfgsvZhjRA1iLdzsKphAKweMSdnI5t6rnQfxlGhfCL6EtQMFjvhzmw0kIZ6SLzBn+pg==
   dependencies:
-    "@quasar/extras" "^1.16.4"
+    "@quasar/extras" "^1.16.5"
     axios "^1.4.0"
     moment-jalaali "^0.10.0"
-    quasar "^2.12.0"
-    quasar-form-builder "^0.1.98"
+    quasar "^2.12.2"
+    quasar-form-builder "^0.1.106"
     shvl "^3.0.0"
     vue "^3.2.47"
 
-quasar-form-builder@^0.1.98:
-  version "0.1.98"
-  resolved "https://registry.yarnpkg.com/quasar-form-builder/-/quasar-form-builder-0.1.98.tgz#b463efb72c35fa04563a59ce0f5e44125ac164b4"
-  integrity sha512-po5GKqVxLc4vqA4ZZqxKLspk4Wjc8Ia1HknSWgP+oxyuWMZD6l9E9KWYtjsnSitpFvx/n40ZCUBSaeM2UFt0Zg==
+quasar-form-builder@^0.1.106:
+  version "0.1.106"
+  resolved "https://registry.yarnpkg.com/quasar-form-builder/-/quasar-form-builder-0.1.106.tgz#9e8acd0a73153c9e096bcde7f72104f57f954f93"
+  integrity sha512-XwcP17sbYBHhhCoXiG86rOkQ2JU+bbMi+XCaGlDOnS8p7l2j7uTWMj48b50v6eV4VA2ZHPuYbtPYYka19lxJTQ==
   dependencies:
-    "@quasar/extras" "^1.16.4"
-    core-js "^3.31.0"
+    "@quasar/extras" "^1.16.5"
+    core-js "^3.31.1"
     moment-jalaali "^0.10.0"
-    quasar "^2.12.0"
+    quasar "^2.12.2"
     shvl "^3.0.0"
     vue "^3.2.47"
 
@@ -5436,10 +5441,10 @@ quasar-template-builder@^1.0.23:
     vue "^3.2.47"
     vuex "^4.1.0"
 
-quasar-ui-q-page-builder@^0.1.49:
-  version "0.1.49"
-  resolved "https://registry.yarnpkg.com/quasar-ui-q-page-builder/-/quasar-ui-q-page-builder-0.1.49.tgz#c1e7a20f5ade493e0a6f76beee998a34f9307b93"
-  integrity sha512-RHTbh+cEn/Rh6FB3cy5Al+QhxOWmHuunwBYvVbje2rrMDmBZhQw69J2xL7FAjHf5kGFPYhJdxV6eL6sHz70EQw==
+quasar-ui-q-page-builder@^0.1.51:
+  version "0.1.51"
+  resolved "https://registry.yarnpkg.com/quasar-ui-q-page-builder/-/quasar-ui-q-page-builder-0.1.51.tgz#c085d380abd587279830f6d748427f71f4f250b8"
+  integrity sha512-1eYO6nXPMP2zw/rzPg5ckxSSZu5zxw/jeDcbcFwGrZizU3P2gxDqi7zEO/sNHAuQZjVzf47+4L1HWASJphYNpg==
   dependencies:
     "@quasar/extras" "^1.16.1"
     "@vue/compiler-sfc" "^3.2.47"
@@ -5452,6 +5457,11 @@ quasar@^2.11.7, quasar@^2.11.9, quasar@^2.12.0:
   resolved "https://registry.yarnpkg.com/quasar/-/quasar-2.12.0.tgz#f145ad2b677a0925ea9ca6a3b44b5502be1cbd87"
   integrity sha512-B8xoeOWNs/Iv7M+FGRvXGYI1qDnJH8AtIb7RiP+zMfMkBcEp+6HJHU/9ODPemC4yteDjO+HPX2f7OhNZKgsPIw==
 
+quasar@^2.12.2:
+  version "2.12.3"
+  resolved "https://registry.yarnpkg.com/quasar/-/quasar-2.12.3.tgz#dd898cce1bbaf9e02310cc7e777c9c42a836a003"
+  integrity sha512-9KK5TXWGsZ6XFF6MJQ0E98yp++D1PagqSJrKpE91A6QnZ1gng4KtkFI5iPyuqXciFpXvxniiLsZtxD5ZICHvPw==
+
 queue-microtask@^1.2.2:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"