Просмотр исходного кода

content and media and session and sessionTemplate

ali 3 лет назад
Родитель
Сommit
6e180d6092
35 измененных файлов с 1546 добавлено и 151 удалено
  1. 1 1
      quasar.config.js
  2. 2 0
      src/api/models/Content.js
  3. 1 0
      src/api/models/Media.js
  4. 1 0
      src/components/Auth/LoginStep.vue
  5. 26 10
      src/components/FormBuilderCustumComponents/ContentCategorySelector.vue
  6. 78 39
      src/components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue
  7. 75 0
      src/components/FormBuilderCustumComponents/ContentMedias/MediaCategory.vue
  8. 84 0
      src/components/FormBuilderCustumComponents/ContentMedias/MediaItem.vue
  9. 121 0
      src/components/FormBuilderCustumComponents/ContentsSelector/ContentItem.vue
  10. 80 0
      src/components/FormBuilderCustumComponents/ContentsSelector/ContentsSelector.vue
  11. 70 0
      src/components/FormBuilderCustumComponents/ContentsSelector/MediaCategory.vue
  12. 205 0
      src/components/FormBuilderCustumComponents/ContentsSelector/SelectorComponent.vue
  13. 4 4
      src/components/Widgets/Content/AdminContentCreate/AdminContentCreate.vue
  14. 2 7
      src/components/Widgets/Content/AdminContentShow/AdminContentShow.vue
  15. 124 0
      src/components/Widgets/Media/AdminMediaShow/AdminMediaShow.vue
  16. 21 0
      src/components/Widgets/Media/AdminMediaShow/OptionPanel.vue
  17. 104 0
      src/components/Widgets/SessionTemplate/AdminSessionTemplateCreate/AdminContentCreate.vue
  18. 21 0
      src/components/Widgets/SessionTemplate/AdminSessionTemplateCreate/OptionPanel.vue
  19. 106 0
      src/components/Widgets/SessionTemplate/AdminSessionTemplateList/AdminContentList.vue
  20. 21 0
      src/components/Widgets/SessionTemplate/AdminSessionTemplateList/OptionPanel.vue
  21. 116 0
      src/components/Widgets/SessionTemplate/AdminSessionTemplateShow/AdminSessionTemplateShow.vue
  22. 21 0
      src/components/Widgets/SessionTemplate/AdminSessionTemplateShow/OptionPanel.vue
  23. 1 0
      src/css/Theme/Controls/controles.scss
  24. 16 0
      src/css/Theme/Controls/select.scss
  25. 6 2
      src/models/Content.js
  26. 8 4
      src/models/Media.js
  27. 11 4
      src/pages/Admin/Classroom/show.vue
  28. 23 0
      src/pages/Admin/Media/Create.vue
  29. 23 0
      src/pages/Admin/Media/List.vue
  30. 23 0
      src/pages/Admin/Media/Show.vue
  31. 23 0
      src/pages/Admin/Media/Ticket.vue
  32. 11 2
      src/pages/Admin/Session/show.vue
  33. 16 9
      src/pages/Admin/SessionTemplate/show.vue
  34. 89 68
      src/pages/Admin/Unit/show.vue
  35. 12 1
      src/router/routes.js

+ 1 - 1
quasar.config.js

@@ -228,7 +228,7 @@ module.exports = configure(function (ctx) {
     // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
     devServer: {
       https: true,
-      port: 8083,
+      port: 8080,
       open: true, // opens browser window automatically
       proxy: {
         [process.env.APP_API]: {

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

@@ -28,6 +28,8 @@ export default class ContentAPI extends APIRepository {
       api: this.api,
       request: this.APIAdresses.base,
       data: this.getNormalizedSendData({
+        category: null, // Number
+        per_page: 10, // Number
         page: 1 // Number
       }, data),
       resolveCallback: (response) => {

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

@@ -7,6 +7,7 @@ export default class MediaAPI extends APIRepository {
     super('media', appApi)
     this.APIAdresses = {
       base: '/cma/medias',
+      byId: (id) => '/cma/medias/' + id,
       assignService: (id) => '/core/packages/' + id + '/assign'
     }
     this.CacheList = {

+ 1 - 0
src/components/Auth/LoginStep.vue

@@ -42,6 +42,7 @@
     <q-btn class="full-width btn-login"
            color="primary"
            label="ورود به سامانه"
+           :loading="loadingList"
            @click="login" />
     <div>
       حساب کاربری ندارید؟

+ 26 - 10
src/components/FormBuilderCustumComponents/ContentCategorySelector.vue

@@ -57,7 +57,11 @@ export default {
   },
   watch: {
     value () {
-      this.inputData = this.value
+      if (this.value.id) {
+        this.getCategories()
+      } else {
+        this.inputData = this.value
+      }
     },
     mainCategory (newValue) {
       this.subCategory = null
@@ -86,13 +90,25 @@ export default {
       this.$emit('update:value', categoryId)
     }
   },
-  created () {
-    this.inputData = this.value
-  },
-  mounted() {
-    this.getCategories()
-  },
   methods: {
+    setMainCategory () {
+      if (!this.value?.parent?.parent?.id) {
+        return
+      }
+      this.mainCategory = this.mainCategoryOptions.find(item => item.value === this.value.parent.parent.id)
+    },
+    setSubCategory () {
+      if (!this.value?.parent?.id) {
+        return
+      }
+      this.subCategory = this.subCategoryOptions.find(item => item.value === this.value.parent.id)
+    },
+    setBakhshCategory () {
+      if (!this.value?.id) {
+        return
+      }
+      this.bakhshCategory = this.bakhshCategoryOptions.find(item => item.value === this.value.id)
+    },
     getCategories () {
       this.contentCategories.laoding = true
       APIGateway.contentCategory.index({ parent__isnull: 'true', per_page: 99999 })
@@ -109,11 +125,11 @@ export default {
 
           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.setMainCategory()
               this.$nextTick(() => {
-                this.subCategory = this.subCategoryOptions.find(item => item.value === this.value.parent.id)
+                this.setSubCategory()
                 this.$nextTick(() => {
-                  this.bakhshCategory = this.bakhshCategoryOptions.find(item => item.value === this.value.id)
+                  this.setBakhshCategory()
                   this.$emit('update:value', this.value.id)
                 })
               })

+ 78 - 39
src/components/FormBuilderCustumComponents/ContentMedias.vue → src/components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue

@@ -1,29 +1,7 @@
 <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>
+  <div class="ContentMedias">
+    <div v-if="canAddMedia"
+         class="q-mb-md">
       <entity-create ref="entityCreate"
                      v-model:value="inputs"
                      title="تعریف چند رسانه ای"
@@ -41,18 +19,44 @@
              @click="entityCreate">
         افزودن چند رسانه ای
       </q-btn>
-    </q-card-section>
-  </q-card>
+    </div>
+    <media-category v-if="imageMedias.list.length > 0"
+                    :medias="imageMedias"
+                    icon="isax:image"
+                    title="تصویر"
+                    class="q-mt-lg"
+                    :can-delete="canDelete"
+                    @onView="onView"
+                    @onDelete="onDelete" />
+    <media-category v-if="soundMedias.list.length > 0"
+                    :medias="soundMedias"
+                    icon="isax:microphone"
+                    title="صوت"
+                    class="q-mt-lg"
+                    :can-delete="canDelete"
+                    @onView="onView"
+                    @onDelete="onDelete" />
+    <media-category v-if="videoMedias.list.length > 0"
+                    :medias="videoMedias"
+                    icon="isax:play"
+                    title="ویدیو"
+                    class="q-mt-lg"
+                    :can-delete="canDelete"
+                    @onView="onView"
+                    @onDelete="onDelete" />
+  </div>
 </template>
 
 <script>
 import { EntityCreate } from 'quasar-crud'
 import { APIGateway } from 'src/api/APIGateway.js'
 import { Media, MediaList } from 'src/models/Media.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import MediaCategory from 'src/components/FormBuilderCustumComponents/ContentMedias/MediaCategory.vue'
 
 export default {
   name: 'ContentMedias',
-  components: { EntityCreate },
+  components: { MediaCategory, EntityCreate },
   props: {
     value: {
       default: () => [],
@@ -61,6 +65,18 @@ export default {
     disable: {
       default: false,
       type: Boolean
+    },
+    canDelete: {
+      type: Boolean,
+      default: true
+    },
+    canAddMedia: {
+      type: Boolean,
+      default: true
+    },
+    sourceType: {
+      type: String,
+      default: 'CONTENT'
     }
   },
   emits: ['update:value'],
@@ -71,9 +87,9 @@ export default {
       entityIdKey: 'id',
       entityParamKey: 'id',
       inputs: [
+        { type: 'file', name: 'file', responseKey: 'file', label: 'فایل', placeholder: ' ', col: 'col-md-4 col-12' },
         { 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' },
@@ -82,20 +98,46 @@ export default {
       entityCreateLoading: false
     }
   },
+  computed: {
+    videoMedias () {
+      return new MediaList(this.medias.getType('VIDEO'))
+    },
+    soundMedias () {
+      return new MediaList(this.medias.getType('SOUND'))
+    },
+    imageMedias () {
+      return new MediaList(this.medias.getType('IMAGE'))
+    }
+  },
   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))
-      }
+    value: {
+      handler () {
+        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))
+        }
+      },
+      immediate: true
     }
   },
   created () {
+    FormBuilderAssist.setAttributeByName(this.inputs, 'source_type', 'value', this.sourceType)
     this.inputData = this.value
   },
   methods: {
-    removeMedia (mediaIndex) {
+    onView (media) {
+      const routeData = this.$router.resolve({ name: 'Admin.Media.Show', params: { id: media.id } })
+      window.open(routeData.href, '_blank')
+    },
+    onDelete (media) {
+      this.removeMedia(media.id)
+    },
+    removeMedia (mediaId) {
+      const mediaIndex = this.medias.list.findIndex(item => item.id === mediaId)
+      if (mediaIndex === -1) {
+        return
+      }
       this.medias.list.splice(mediaIndex, 1)
       this.$emit('update:value', this.medias.list.map(item => item.id))
     },
@@ -117,8 +159,5 @@ export default {
 
 <style lang="scss" scoped>
 .ContentMedias {
-  :deep(.media-item) {
-    display: block;
-  }
 }
 </style>

+ 75 - 0
src/components/FormBuilderCustumComponents/ContentMedias/MediaCategory.vue

@@ -0,0 +1,75 @@
+<template>
+  <div class="MediaCategory">
+    <div class="title">
+      <q-icon :name="icon"
+              size="19px" />
+      {{ title }}
+    </div>
+    <div class="media-list row q-col-gutter-md">
+      <div v-for="media in medias.list"
+           :key="media.id"
+           class="media-item col-md-3 col-sm-6 col-12">
+        <media-item :media="media"
+                    :can-delete="canDelete"
+                    @onView="onView(media)"
+                    @onDelete="onDelete(media)" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { MediaList } from 'src/models/Media.js'
+import MediaItem from 'src/components/FormBuilderCustumComponents/ContentMedias/MediaItem.vue'
+
+export default {
+  name: 'MediaCategory',
+  components: { MediaItem },
+  props: {
+    medias: {
+      type: MediaList,
+      default: new MediaList()
+    },
+    title: {
+      type: String,
+      default: ''
+    },
+    icon: {
+      type: String,
+      default: ''
+    },
+    canDelete: {
+      type: Boolean,
+      default: true
+    }
+  },
+  emits: ['update:value'],
+  data () {
+    return {
+      entityCreateLoading: false
+    }
+  },
+  methods: {
+    onView (media) {
+      this.$emit('onView', media)
+    },
+    onDelete (media) {
+      this.$emit('onDelete', media)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.MediaCategory {
+  .title {
+    font-size: 16px;
+    letter-spacing: 0;
+    color: #212121;
+    margin-bottom: 26px;
+    .q-icon {
+      margin-left: 9px;
+    }
+  }
+}
+</style>

+ 84 - 0
src/components/FormBuilderCustumComponents/ContentMedias/MediaItem.vue

@@ -0,0 +1,84 @@
+<template>
+  <div class="MediaItem">
+    <div class="title ellipsis">
+      {{ media.title }}
+    </div>
+    <div class="actions">
+      <q-btn icon="isax:eye"
+             outline
+             class="viewBtn"
+             color="primary"
+             @click="onView" />
+      <q-btn v-if="canDelete"
+             class="deleteBtn"
+             icon="isax:trash4"
+             outline
+             color="red"
+             @click="onDelete" />
+    </div>
+  </div>
+</template>
+
+<script>
+import { Media } from 'src/models/Media.js'
+
+export default {
+  name: 'MediaItem',
+  props: {
+    media: {
+      type: Media,
+      default: new Media()
+    },
+    canDelete: {
+      type: Boolean,
+      default: true
+    }
+  },
+  emits: ['update:value'],
+  data () {
+    return {
+      entityCreateLoading: false
+    }
+  },
+  methods: {
+    onView () {
+      this.$emit('onView')
+    },
+    onDelete () {
+      this.$emit('onDelete')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.MediaItem {
+  background: #F9F9F9;
+  border: 1px solid #DFE1EC;
+  border-radius: 10px;
+  padding: 10px 16px 10px 21px;
+  display: flex;
+  flex-flow: row;
+  justify-content: space-between;
+  align-items: center;
+  .title {
+    font-size: 15px;
+    letter-spacing: 0;
+    color: #212121;
+  }
+  .actions {
+    display: flex;
+    flex-flow: row;
+    justify-content: flex-end;
+    .q-btn {
+      width: 42px;
+      height: 42px;
+      &.viewBtn {
+      }
+      &.deleteBtn {
+        margin-left: 16px;
+      }
+    }
+  }
+}
+</style>

+ 121 - 0
src/components/FormBuilderCustumComponents/ContentsSelector/ContentItem.vue

@@ -0,0 +1,121 @@
+<template>
+  <div class="ContentItem">
+    <q-expansion-item expand-separator
+                      :label="content.title">
+      <q-card>
+        <q-card-section>
+          <div class="content-info">
+            <div class="title-and-thumbnail">
+              <div class="thumbnail">
+                <q-img :src="content.thumbnail"
+                       width="100px" />
+              </div>
+              <div class="title ellipsis-2-lines">
+                {{ content.title }}
+              </div>
+            </div>
+            <div class="actions">
+              <q-btn icon="isax:eye"
+                     outline
+                     color="primary"
+                     class="viewBtn"
+                     @click="onView(content)" />
+              <q-btn icon="isax:trash4"
+                     outline
+                     color="red"
+                     @click="onDelete(content)" />
+            </div>
+          </div>
+          <div class="content-medias">
+            <content-medias :value="content.medias_info"
+                            :can-delete="false"
+                            :can-add-media="false" />
+          </div>
+        </q-card-section>
+      </q-card>
+    </q-expansion-item>
+  </div>
+</template>
+
+<script>
+import { Content } from 'src/models/Content.js'
+import ContentMedias from 'src/components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue'
+
+export default {
+  name: 'ContentItem',
+  components: { ContentMedias },
+  props: {
+    content: {
+      type: Content,
+      default: new Content()
+    }
+  },
+  emits: ['update:value'],
+  data () {
+    return {
+      mediasInfo: [],
+      entityCreateLoading: false
+    }
+  },
+  methods: {
+    onView (content) {
+      this.$emit('onView', content)
+    },
+    onDelete (content) {
+      this.$emit('onDelete', content)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.ContentItem {
+  :deep(.q-expansion-item__container) {
+    .q-item {
+      display: flex !important;
+      border: solid 1px rgba(0, 0, 0, 0.12);
+      border-radius: 6px;
+    }
+    .q-expansion-item__content {
+      border: solid 1px rgba(0, 0, 0, 0.12);
+      border-radius: 6px;
+    }
+  }
+  .content-info {
+    display: flex;
+    flex-flow: row;
+    justify-content: space-between;
+    align-items: center;
+    .title-and-thumbnail {
+      display: flex;
+      flex-flow: row;
+      justify-content: flex-start;
+      align-items: center;
+      .thumbnail {
+        width: 100px;
+        margin-right: 10px;
+      }
+      .title {
+        font-size: 15px;
+        letter-spacing: 0;
+        color: #212121;
+      }
+    }
+    .actions {
+      display: flex;
+      flex-flow: row;
+      justify-content: flex-end;
+      .q-btn {
+        width: 42px;
+        height: 42px;
+        &.viewBtn {
+          margin-right: 16px;
+        }
+      }
+    }
+    .content-medias {
+
+    }
+  }
+}
+</style>

+ 80 - 0
src/components/FormBuilderCustumComponents/ContentsSelector/ContentsSelector.vue

@@ -0,0 +1,80 @@
+<template>
+  <div class="ContentsSelector">
+    <div class="ContentCategorySelector">
+      <selector-component v-model:contents="selectedContents"
+                          @update:contents="onSelectContent" />
+    </div>
+    <div class="content-list q-mt-lg">
+      <content-item v-for="(selectedContent, selectedContentIndex) in selectedContents"
+                    :key="selectedContentIndex"
+                    :content="selectedContent"
+                    @onView="onView"
+                    @onDelete="onDelete" />
+    </div>
+  </div>
+</template>
+
+<script>
+import { ContentList } from 'src/models/Content.js'
+import ContentItem from 'src/components/FormBuilderCustumComponents/ContentsSelector/ContentItem.vue'
+import SelectorComponent from 'src/components/FormBuilderCustumComponents/ContentsSelector/SelectorComponent.vue'
+
+export default {
+  name: 'ContentsSelector',
+  components: {
+    ContentItem,
+    SelectorComponent
+  },
+  props: {
+    value: {
+      default: () => [],
+      type: [Array, String, Number, Boolean]
+    },
+    disable: {
+      default: false,
+      type: Boolean
+    }
+  },
+  emits: ['update:value'],
+  data () {
+    return {
+      selectedContents: []
+    }
+  },
+  watch: {
+    value: {
+      handler (newValue) {
+        if (newValue.length > 0 && newValue[0].id) {
+          this.selectedContents = (new ContentList(JSON.parse(JSON.stringify(newValue)))).list
+        }
+      },
+      immediate: true
+    }
+  },
+  methods: {
+    onSelectContent () {
+      this.$emit('update:value', this.selectedContents.map(item => item.id))
+    },
+    onView (content) {
+      const routeData = this.$router.resolve({ name: 'Admin.Content.Show', params: { id: content.id } })
+      window.open(routeData.href, '_blank')
+    },
+    onDelete (content) {
+      this.removeContent(content.id)
+    },
+    removeContent (mediaId) {
+      const contentIndex = this.selectedContents.findIndex(item => item.id === mediaId)
+      if (contentIndex === -1) {
+        return
+      }
+      this.selectedContents.splice(contentIndex, 1)
+      this.$emit('update:value', this.selectedContents.map(item => item.id))
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.ContentsSelector {
+}
+</style>

+ 70 - 0
src/components/FormBuilderCustumComponents/ContentsSelector/MediaCategory.vue

@@ -0,0 +1,70 @@
+<template>
+  <div class="MediaCategory">
+    <div class="title">
+      <q-icon :name="icon"
+              size="19px" />
+      {{ title }}
+    </div>
+    <div class="media-list row q-col-gutter-md">
+      <div v-for="media in medias.list"
+           :key="media.id"
+           class="media-item col-md-3 col-sm-6 col-12">
+        <media-item :media="media"
+                    @onView="onView(media)"
+                    @onDelete="onDelete(media)" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { MediaList } from 'src/models/Media.js'
+import MediaItem from 'src/components/FormBuilderCustumComponents/ContentMedias/MediaItem.vue'
+
+export default {
+  name: 'MediaCategory',
+  components: { MediaItem },
+  props: {
+    medias: {
+      type: MediaList,
+      default: new MediaList()
+    },
+    title: {
+      type: String,
+      default: ''
+    },
+    icon: {
+      type: String,
+      default: ''
+    }
+  },
+  emits: ['update:value'],
+  data () {
+    return {
+      entityCreateLoading: false
+    }
+  },
+  methods: {
+    onView (media) {
+      this.$emit('onView', media)
+    },
+    onDelete (media) {
+      this.$emit('onDelete', media)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.MediaCategory {
+  .title {
+    font-size: 16px;
+    letter-spacing: 0;
+    color: #212121;
+    margin-bottom: 26px;
+    .q-icon {
+      margin-left: 9px;
+    }
+  }
+}
+</style>

+ 205 - 0
src/components/FormBuilderCustumComponents/ContentsSelector/SelectorComponent.vue

@@ -0,0 +1,205 @@
+<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"
+                @update:model-value="onChangeBakhsh" />
+    </div>
+    <div class="col-12">
+      <div>
+        انتخاب منابع
+      </div>
+      <q-select v-model="localContents"
+                :options="contentOptions.list"
+                :option-label="'title'"
+                :option-value="'id'"
+                multiple
+                :loading="contentOptions.laoding" />
+    </div>
+  </div>
+</template>
+
+<script>
+import { APIGateway } from 'src/api/APIGateway.js'
+import { ContentList } from 'src/models/Content.js'
+import { ContentCategoryList } from 'src/models/ContentCategory.js'
+
+export default {
+  name: 'SelectorComponent',
+  props: {
+    contents: {
+      default: () => [],
+      type: Array
+    }
+  },
+  emits: ['update:contents'],
+  data () {
+    return {
+      initialDataLoaded: false,
+      contentOptions: new ContentList(),
+      mainCategory: null,
+      subCategory: null,
+      bakhshCategory: null,
+      mainCategoryOptions: [],
+      subCategoryOptions: [],
+      bakhshCategoryOptions: [],
+      contentCategories: new ContentCategoryList()
+    }
+  },
+  computed: {
+    localContents: {
+      get () {
+        return this.contents
+      },
+      set (newValue) {
+        if (newValue.length === 0) {
+          console.trace()
+        }
+        this.$emit('update:contents', newValue)
+      }
+    }
+  },
+  watch: {
+    contents: {
+      handler () {
+        this.setCategories()
+      },
+      immediate: true
+    },
+    mainCategory (newValue) {
+      this.subCategory = null
+      this.subCategoryOptions = []
+      if (!newValue) {
+        return
+      }
+      this.subCategoryOptions = newValue.item.children.map(item => {
+        return {
+          item,
+          label: item.title,
+          value: item.id
+        }
+      })
+    },
+    subCategory (newValue) {
+      this.bakhshCategory = null
+      this.bakhshCategoryOptions = []
+      if (!newValue) {
+        return
+      }
+      this.bakhshCategoryOptions = newValue.item.children.map(item => {
+        return {
+          item,
+          label: item.title,
+          value: item.id
+        }
+      })
+    },
+    bakhshCategory (newValue) {
+      if (!newValue) {
+        return
+      }
+      const categoryId = newValue ? newValue.value : null
+      this.getContents(categoryId)
+    }
+  },
+  mounted() {
+    this.getCategories()
+  },
+  methods: {
+    setMainCategory () {
+      const firstContent = (this.localContents && this.localContents.length > 0) ? this.localContents[0] : null
+      if (!firstContent?.category_info?.parent?.parent?.id) {
+        return
+      }
+      this.mainCategory = this.mainCategoryOptions.find(item => item.value === firstContent?.category_info.parent.parent.id)
+    },
+    setSubCategory () {
+      const firstContent = (this.localContents && this.localContents.length > 0) ? this.localContents[0] : null
+      if (!firstContent?.category_info?.parent?.id) {
+        return
+      }
+      this.subCategory = this.subCategoryOptions.find(item => item.value === firstContent?.category_info.parent.id)
+    },
+    setBakhshCategory () {
+      const firstContent = (this.localContents && this.localContents.length > 0) ? this.localContents[0] : null
+      if (!firstContent?.category_info?.id) {
+        return
+      }
+      this.bakhshCategory = this.bakhshCategoryOptions.find(item => item.value === firstContent?.category_info.id)
+    },
+    setCategories () {
+      this.$nextTick(() => {
+        this.setMainCategory()
+        this.$nextTick(() => {
+          this.setSubCategory()
+          this.$nextTick(() => {
+            this.setBakhshCategory()
+            this.$nextTick(() => {
+              this.initialDataLoaded = true
+            })
+          })
+        })
+      })
+    },
+    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
+            }
+          })
+        })
+        .catch(() => {
+          this.contentCategories.laoding = false
+        })
+    },
+    getContents (categoryId) {
+      this.contentOptions = new ContentList()
+      this.contentOptions.loading = true
+      APIGateway.content.index({ category: categoryId, per_page: 9999 })
+        .then(contents => {
+          this.contentOptions = new ContentList(contents.list)
+          this.contentOptions.laoding = false
+        })
+        .catch(() => {
+          this.contentOptions.laoding = false
+        })
+    },
+    onChangeBakhsh () {
+      this.localContents = []
+    }
+  }
+
+}
+</script>
+
+<style lang="scss" scoped>
+.ContentCategorySelector {
+}
+</style>

+ 4 - 4
src/components/Widgets/Content/AdminContentCreate/AdminContentCreate.vue

@@ -23,7 +23,7 @@ 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 ContentMedias from 'src/components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue'
 import ContentCategorySelector from 'src/components/FormBuilderCustumComponents/ContentCategorySelector.vue'
 
 const BtnControlComp = shallowRef(BtnControl)
@@ -45,9 +45,9 @@ export default {
       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: ContentCategorySelectorComp, name: 'category', responseKey: 'category', col: 'col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', col: 'col-12' },
+        { type: ContentMediasComp, name: 'medias', responseKey: 'medias_info', col: 'col-12' },
         { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ایجاد محتوا', placeholder: ' ', atClick: () => {}, col: 'col-md-6' }
       ]
     }

+ 2 - 7
src/components/Widgets/Content/AdminContentShow/AdminContentShow.vue

@@ -25,7 +25,7 @@ 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 ContentMedias from 'components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue'
 import ContentCategorySelector from 'src/components/FormBuilderCustumComponents/ContentCategorySelector.vue'
 
 const BtnControlComp = shallowRef(BtnControl)
@@ -49,7 +49,7 @@ export default {
       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: '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' },
@@ -57,11 +57,6 @@ export default {
       ]
     }
   },
-  computed: {
-    repliesInfo () {
-      return this.inputs.find(input => input.name === 'replies_info').value
-    }
-  },
   created() {
     this.api = APIGateway.content.APIAdresses.byId(this.$route.params.id)
   },

+ 124 - 0
src/components/Widgets/Media/AdminMediaShow/AdminMediaShow.vue

@@ -0,0 +1,124 @@
+<template>
+  <div class="AdminMediaShow"
+       :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 { Media } from 'src/models/Media.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import BtnControl from 'src/components/Control/btn.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+export default {
+  name: 'AdminMediaShow',
+  components: {
+    EntityEdit
+  },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      mounted: false,
+      entityLoading: true,
+      api: null,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Content.Show',
+      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' },
+        { 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.media.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">
+.AdminMediaShow {
+  .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/Media/AdminMediaShow/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>

+ 104 - 0
src/components/Widgets/SessionTemplate/AdminSessionTemplateCreate/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 'components/FormBuilderCustumComponents/ContentMedias/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/SessionTemplate/AdminSessionTemplateCreate/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/SessionTemplate/AdminSessionTemplateList/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/SessionTemplate/AdminSessionTemplateList/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>

+ 116 - 0
src/components/Widgets/SessionTemplate/AdminSessionTemplateShow/AdminSessionTemplateShow.vue

@@ -0,0 +1,116 @@
+<template>
+  <div class="AdminSessionTemplateShow"
+       :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 { EntityEdit } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'AdminSessionTemplateShow',
+  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-9 col-12' },
+        { type: 'separator', name: 'space', size: '0', col: 'col-12' },
+        { type: 'dateTime', name: 'ending_enrollment_period', responseKey: 'ending_enrollment_period', label: 'زمان شروع جلسه', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'dateTime', name: 'ending_enrollment_period', responseKey: 'ending_enrollment_period', label: 'زمان پایان جلسه', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'inputEditor', name: 'syllabus', responseKey: 'syllabus', label: 'مقرری', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'inputEditor', name: 'assignment_description', responseKey: 'assignment_description', label: 'تکلیف جلسه', placeholder: ' ', col: 'col-12' },
+        { type: 'hidden', name: 'id', responseKey: 'id', label: 'id', col: 'col-12' }
+      ]
+    }
+  },
+  created() {
+    this.api = APIGateway.sessionTemplate.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">
+.AdminSessionTemplateShow {
+  .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/SessionTemplate/AdminSessionTemplateShow/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>

+ 1 - 0
src/css/Theme/Controls/controles.scss

@@ -3,3 +3,4 @@
 @import 'radioButton';
 @import 'button';
 @import 'toggle';
+@import 'select';

+ 16 - 0
src/css/Theme/Controls/select.scss

@@ -0,0 +1,16 @@
+.q-select {
+  .q-field__inner {
+    .q-field__control {
+      .q-field__control-container {
+        .q-field__native {
+          min-height: 48px;
+          padding-left: 20px;
+        }
+      }
+      .q-field__append {
+        min-height: 48px;
+        height: 48px;
+      }
+    }
+  }
+}

+ 6 - 2
src/models/Content.js

@@ -1,7 +1,7 @@
 import { Collection, Model } from 'js-abstract-model'
 import { CelebrityList } from 'src/models/Celebrity.js'
+import { ContentCategory } from 'src/models/ContentCategory.js'
 import { ContentHashtagList } from 'src/models/ContentHashtag.js'
-import { ContentCategoryList } from 'src/models/ContentCategory.js'
 
 class Content extends Model {
   constructor (data) {
@@ -19,10 +19,14 @@ class Content extends Model {
         key: 'medias',
         defalut: []
       },
+      {
+        key: 'medias_info',
+        defalut: []
+      },
       { key: 'category' },
       {
         key: 'category_info',
-        relatedModel: ContentCategoryList
+        relatedModel: ContentCategory
       },
       {
         key: 'celebrities',

+ 8 - 4
src/models/Media.js

@@ -36,10 +36,10 @@ class Media extends Model {
         label: 'ویدیو',
         value: 'VIDEO'
       },
-      {
-        label: 'متن',
-        value: 'TEXT'
-      },
+      // {
+      //   label: 'متن',
+      //   value: 'TEXT'
+      // },
       {
         label: 'صوت',
         value: 'SOUND'
@@ -67,6 +67,10 @@ class MediaList extends Collection {
   model () {
     return Media
   }
+
+  getType (type) {
+    return this.list.filter(item => item.type === type)
+  }
 }
 
 export { Media, MediaList }

+ 11 - 4
src/pages/Admin/Classroom/show.vue

@@ -21,11 +21,11 @@
 
     <q-separator />
 
-    <q-tab-panels v-if="mounted"
-                  v-model="tab"
+    <q-tab-panels v-model="tab"
                   animated>
       <q-tab-panel name="classroomInfo">
-        <entity-edit ref="classroomEntityEdit"
+        <entity-edit v-if="mounted"
+                     ref="classroomEntityEdit"
                      :key="classroomEntityEditKey"
                      v-model:value="inputs"
                      title="مشخصات دوره آموزشی"
@@ -47,9 +47,13 @@
             </div>
           </template>
         </entity-edit>
+        <q-skeleton v-else
+                    type="rect"
+                    height="200px" />
       </q-tab-panel>
       <q-tab-panel name="educations">
-        <entity-index v-model:value="sessionListInputs"
+        <entity-index v-if="mounted"
+                      v-model:value="sessionListInputs"
                       title="لیست جلسات"
                       :api="sessionListApi"
                       :table="sessionListTable"
@@ -73,6 +77,9 @@
             </template>
           </template>
         </entity-index>
+        <q-skeleton v-else
+                    type="rect"
+                    height="200px" />
       </q-tab-panel>
 
       <q-tab-panel name="movies1">

+ 23 - 0
src/pages/Admin/Media/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/Media/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/Media/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: 'AdminMediaShow' }], 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/Media/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>

+ 11 - 2
src/pages/Admin/Session/show.vue

@@ -25,8 +25,14 @@
 </template>
 
 <script>
+import { shallowRef } from 'vue'
 import { EntityEdit } from 'quasar-crud'
 import { APIGateway } from 'src/api/APIGateway.js'
+import ContentMedias from 'src/components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue'
+import ContentsSelector from 'src/components/FormBuilderCustumComponents/ContentsSelector/ContentsSelector.vue'
+
+const ContentMediasComp = shallowRef(ContentMedias)
+const ContentsSelectorComp = shallowRef(ContentsSelector)
 
 export default {
   name: 'Admin.Session.Show',
@@ -50,7 +56,10 @@ export default {
         { type: 'dateTime', name: 'ending_enrollment_period', responseKey: 'ending_enrollment_period', label: 'زمان پایان جلسه', placeholder: ' ', col: 'col-md-6 col-12' },
         { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', placeholder: ' ', col: 'col-md-6 col-12' },
         { type: 'inputEditor', name: 'syllabus', responseKey: 'syllabus', label: 'مقرری', placeholder: ' ', col: 'col-md-6 col-12' },
-        { type: 'inputEditor', name: 'assignment_description', responseKey: 'assignment_description', label: 'تکلیف جلسه', placeholder: ' ', col: 'col-12' },
+        { type: ContentsSelectorComp, name: 'contents', responseKey: 'contents_info', col: 'col-12' },
+        { type: ContentMediasComp, name: 'medias', responseKey: 'medias_info', sourceType: 'SESSION', col: 'col-12' },
+        { type: 'inputEditor', name: 'assignment_description', responseKey: 'assignment_description', label: 'تکلیف', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'inputEditor', name: 'assignment_answer', responseKey: 'assignment_answer', label: 'پاسخ تکلیف', placeholder: ' ', col: 'col-md-6 col-12' },
         { type: 'hidden', name: 'id', responseKey: 'id', label: 'id', col: 'col-12' }
       ]
     }
@@ -61,7 +70,7 @@ export default {
   },
   methods: {
     updateSessionTemplates () {
-      this.$refs.sessionEntityEdit.editEntity()
+      this.$refs.sessionEntityEdit.editEntity(false)
     },
     getRemoveMessage (row) {
       return 'آیا از حذف ' + row.title + ' اطمینان دارید؟'

+ 16 - 9
src/pages/Admin/SessionTemplate/show.vue

@@ -22,8 +22,12 @@
 </template>
 
 <script>
+import { shallowRef } from 'vue'
 import { EntityEdit } from 'quasar-crud'
-import API_ADDRESS from 'src/api/Addresses.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import ContentsSelector from 'src/components/FormBuilderCustumComponents/ContentsSelector/ContentsSelector.vue'
+
+const ContentsSelectorComp = shallowRef(ContentsSelector)
 
 export default {
   name: 'Admin.SessionTemplates.Index',
@@ -35,22 +39,25 @@ export default {
       newUnitLoading: false,
       newUnitName: null,
       newUnitSessionCount: null,
-      api: API_ADDRESS.sessionTemplates.base,
+      api: null,
       entityIdKey: 'id',
       entityParamKey: 'id',
       showRouteName: 'Admin.SessionTemplate.Show',
       inputs: [
-        { type: 'input', name: 'title', responseKey: 'title', label: 'نام جلسه', col: 'col-md-6' },
+        { type: 'input', name: 'title', responseKey: 'title', label: 'نام جلسه', placeholder: ' ', col: 'col-md-6 col-12' },
         { type: 'separator', name: 'space', size: '0', col: 'col-md-12' },
-        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', col: 'col-md-6' },
-        { type: 'inputEditor', name: 'syllabus', responseKey: 'syllabus', label: 'مقرری', col: 'col-md-6' },
-        { type: 'hidden', name: 'id', responseKey: 'id', label: 'id', col: 'col-md-12' },
-        { type: 'hidden', name: 'unit', responseKey: 'unit', label: 'unit', col: 'col-md-12' }
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'inputEditor', name: 'syllabus', responseKey: 'syllabus', label: 'مقرری', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: ContentsSelectorComp, name: 'contents', responseKey: 'contents_info', col: 'col-12' },
+        { type: 'inputEditor', name: 'assignment_question', responseKey: 'assignment_question', label: 'تکلیف', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'inputEditor', name: 'assignment_answer', responseKey: 'assignment_answer', label: 'پاسخ تکلیف', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'hidden', name: 'id', responseKey: 'id' },
+        { type: 'hidden', name: 'unit', responseKey: 'unit' }
       ]
     }
   },
   created () {
-    this.api += '/' + this.$route.params.id
+    this.api = APIGateway.sessionTemplate.APIAdresses.byId(this.$route.params.id)
   },
   methods: {
     updateSessionTemplates () {
@@ -64,7 +71,7 @@ export default {
     },
     createUnit () {
       this.newUnitLoading = true
-      this.$axios.post(API_ADDRESS.unit.base, {
+      APIGateway.unit.create({
         title: this.newUnitName,
         category: this.$route.params.id,
         default_session_count: this.newUnitSessionCount

+ 89 - 68
src/pages/Admin/Unit/show.vue

@@ -1,75 +1,95 @@
 <template>
-  <div>
-    <div>
-      <entity-edit v-if="mounted"
-                   ref="categoryEntityEdit"
-                   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">
-        <template #after-form-builder>
-          <div class="flex justify-end">
-            <q-btn color="primary"
-                   label="تایید"
-                   @click="updateCategory" />
-          </div>
-        </template>
-      </entity-edit>
-    </div>
-    <div class="q-mt-md">
-      <entity-index v-if="mounted"
-                    ref="sessionEntityIndex"
-                    v-model:value="sessionFilterInputs"
-                    title="لیست جلسات"
-                    :api="sessionApi"
-                    :table="sessionTable"
-                    :table-keys="sessionTableKeys"
-                    :show-reload-button="false"
-                    :show-search-button="false"
-                    :show-expand-button="false">
-        <template #toolbar>
-          <div class="flex justify-end">
-            <q-btn color="primary"
-                   outline
-                   label="افزودن جلسه"
-                   :loading="newSessionLoading"
-                   @click="createSession" />
-          </div>
-        </template>
-        <template #table-cell="{inputData, showConfirmRemoveDialog}">
-          <q-td :props="inputData.props">
-            <template v-if="inputData.props.col.name === 'actions'">
-              <div class="q-gutter-md">
-                <div class="action-column-entity-index">
-                  <q-btn size="md"
-                         color="primary"
-                         outline
-                         label="بانک سوالات"
-                         class="btn-go-to-question-bank"
-                         :to="{name: 'Admin.Session.Show', params: {id: inputData.props.row.id}}" />
-                  <q-btn size="md"
-                         color="primary"
-                         label="تعیین جزییات"
-                         :to="{name: 'Admin.SessionTemplate.Show', params: {id: inputData.props.row.id}}" />
-                  <delete-btn @click="showConfirmRemoveDialog(inputData.props.row, 'id', getRemoveMessage(inputData.props.row))" />
-                </div>
+  <q-card class="AdminUnitShow">
+    <q-tabs v-model="tab"
+            dense
+            class="text-grey"
+            active-color="primary"
+            indicator-color="primary"
+            align="justify"
+            narrow-indicator>
+      <q-tab name="UnitInfo"
+             label="مشخصات" />
+      <q-tab name="sessions"
+             label="لیست جلسات" />
+      <q-tab name="quiz"
+             label="آزمون" />
+    </q-tabs>
+    <q-separator />
+    <q-tab-panels v-if="mounted"
+                  v-model="tab"
+                  animated>
+      <q-tab-panel name="UnitInfo">
+        <entity-edit v-if="mounted"
+                     ref="categoryEntityEdit"
+                     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">
+          <template #after-form-builder>
+            <div class="flex justify-end">
+              <q-btn color="primary"
+                     label="تایید"
+                     @click="updateCategory" />
+            </div>
+          </template>
+        </entity-edit>
+      </q-tab-panel>
+      <q-tab-panel name="sessions">
+        <div class="q-mt-md">
+          <entity-index v-if="mounted"
+                        ref="sessionEntityIndex"
+                        v-model:value="sessionFilterInputs"
+                        title="لیست جلسات"
+                        :api="sessionApi"
+                        :table="sessionTable"
+                        :table-keys="sessionTableKeys"
+                        :show-reload-button="false"
+                        :show-search-button="false"
+                        :show-expand-button="false">
+            <template #toolbar>
+              <div class="flex justify-end">
+                <q-btn color="primary"
+                       outline
+                       label="افزودن جلسه"
+                       :loading="newSessionLoading"
+                       @click="createSession" />
               </div>
             </template>
-            <template v-else>
-              {{ inputData.props.value }}
+            <template #entity-index-table-cell="{inputData, showConfirmRemoveDialog}">
+              <template v-if="inputData.col.name === 'actions'">
+                <div class="q-gutter-md">
+                  <div class="action-column-entity-index">
+                    <q-btn size="md"
+                           color="primary"
+                           outline
+                           label="بانک سوالات"
+                           class="btn-go-to-question-bank"
+                           :to="{name: 'Admin.Session.Show', params: {id: inputData.props.row.id}}" />
+                    <q-btn size="md"
+                           color="primary"
+                           label="تعیین جزییات"
+                           :to="{name: 'Admin.SessionTemplate.Show', params: {id: inputData.props.row.id}}" />
+                    <delete-btn @click="showConfirmRemoveDialog(inputData.props.row, 'id', getRemoveMessage(inputData.props.row))" />
+                  </div>
+                </div>
+              </template>
+              <template v-else>
+                {{ inputData.col.value }}
+              </template>
             </template>
-          </q-td>
-        </template>
-      </entity-index>
-    </div>
-  </div>
+          </entity-index>
+        </div>
+      </q-tab-panel>
+      <q-tab-panel name="quiz" />
+    </q-tab-panels>
+  </q-card>
 </template>
 
 <script>
@@ -92,6 +112,7 @@ export default {
   },
   data () {
     return {
+      tab: 'UnitInfo',
       mounted: false,
       newSessionLoading: false,
       newSessionName: '-',

+ 12 - 1
src/router/routes.js

@@ -255,7 +255,7 @@ const routes = [
           },
           {
             name: 'Admin.SessionTemplate',
-            path: 'sessionTemplates',
+            path: 'session-templates',
             component: () => import('layouts/bareLayout.vue'),
             breadcrumbs: { title: 'طلیعه حکمت' },
             children: [
@@ -309,6 +309,17 @@ const routes = [
               { name: 'Admin.Content.Ticket', path: 'ticket', component: () => import('src/pages/Admin/Classroom/Ticket.vue'), breadcrumbs: { title: 'درخواست ها دوره های آموزشی' } }
             ]
           },
+          {
+            path: 'media',
+            name: 'Admin.Media',
+            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.Media.Show', component: () => import('src/pages/Admin/Media/Show.vue') }
+              // { name: 'Admin.Content.Ticket', path: 'ticket', component: () => import('src/pages/Admin/Classroom/Ticket.vue'), breadcrumbs: { title: 'درخواست ها دوره های آموزشی' } }
+            ]
+          },
           {
             path: 'content-category',
             name: 'Admin.ContentCategory',