ali 2 tahun lalu
induk
melakukan
9579495bdf

+ 2 - 0
src/api/APIGateway.js

@@ -4,6 +4,7 @@ import UserAPI from 'src/api/models/User.js'
 import PostAPI from 'src/api/models/Post.js'
 import UnitAPI from 'src/api/models/Unit.js'
 import TeamAPI from 'src/api/models/Team.js'
+import TestAPI from 'src/api/models/Test.js'
 import EventAPI from 'src/api/models/Event.js'
 import MediaAPI from 'src/api/models/Media.js'
 import TicketAPI from 'src/api/models/Ticket.js'
@@ -43,6 +44,7 @@ export const APIGateway = {
   unit: new UnitAPI(),
   post: new PostAPI(),
   team: new TeamAPI(),
+  test: new TestAPI(),
   event: new EventAPI(),
   media: new MediaAPI(),
   ticket: new TicketAPI(),

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

@@ -22,6 +22,7 @@ export default class SessionTemplateAPI extends APIRepository {
       api: this.api,
       request: this.APIAdresses.base,
       data: this.getNormalizedSendData({
+        unit: null, // Number
         category: null, // Number
         per_page: 10, // Number
         page: 1 // Number

+ 71 - 0
src/api/models/Test.js

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

+ 139 - 0
src/components/FormBuilderCustumComponents/QuestionsSelector/ContentItem.vue

@@ -0,0 +1,139 @@
+<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 v-if="showPageLink"
+                     icon="visibility"
+                     outline
+                     color="primary"
+                     class="viewBtn"
+                     @click="onView(content)" />
+              <q-btn v-if="!readonly"
+                     icon="clear"
+                     outline
+                     color="red"
+                     @click="onDelete(content)" />
+            </div>
+          </div>
+          <q-separator />
+          <div class="content-medias">
+            <content-medias :value="content.medias_info"
+                            :can-delete="false"
+                            :can-add-media="false"
+                            :show-page-link="showPageLink"
+                            :show-download-link="showDownloadLink" />
+          </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()
+    },
+    readonly: {
+      default: false,
+      type: Boolean
+    },
+    showPageLink: {
+      default: true,
+      type: Boolean
+    },
+    showDownloadLink: {
+      default: false,
+      type: Boolean
+    }
+  },
+  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>

+ 70 - 0
src/components/FormBuilderCustumComponents/QuestionsSelector/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>

+ 347 - 0
src/components/FormBuilderCustumComponents/QuestionsSelector/QuestionsSelector.vue

@@ -0,0 +1,347 @@
+<template>
+  <div class="ContentsSelector q-mt-md">
+    <q-banner>
+      انتخاب سوال
+    </q-banner>
+    <div v-if="!readonly">
+      <div class="row q-col-gutter-md q-mt-xs">
+        <div class="col-md-4">
+          <div>
+            نوع جلسه
+          </div>
+          <div>
+            <q-select v-model="selectedSession"
+                      :options="sessionOptions"
+                      :loading="sessions.loading"
+                      emit-value
+                      map-options />
+          </div>
+        </div>
+        <div class="col-md-4 flex items-end">
+          <q-btn color="primary"
+                 label="تایید فیلتر"
+                 @click="filterQuestionList" />
+        </div>
+      </div>
+    </div>
+    <div class="content-list q-mt-lg">
+      <entity-index v-if="selectedSessionId"
+                    ref="entityIndexQuestions"
+                    v-model:value="inputs"
+                    :title="'لیست سوالات ' + selectedSession?.title"
+                    :api="api"
+                    :table="table"
+                    :table-keys="tableKeys"
+                    :show-search-button="false"
+                    :show-expand-button="false"
+                    :show-reload-button="false"
+                    @onPageChanged="onChangeQuestionListPage">
+        <template #entity-index-table-cell="{inputData}">
+          <template v-if="inputData.col.name === 'choice'">
+            <q-checkbox v-model="loadedQuestions[inputData.props.row.id].selected"
+                        color="primary"
+                        @update:model-value="emitValues" />
+          </template>
+          <template v-else-if="inputData.col.name === 'mark'">
+            <q-input v-model="loadedQuestions[inputData.props.row.id].mark"
+                     :disable="!loadedQuestions[inputData.props.row.id].selected"
+                     style="width: 80px"
+                     @update:model-value="emitValues" />
+          </template>
+          <template v-else-if="inputData.col.name === 'number'">
+            {{ inputData.rowNumber }}
+          </template>
+          <template v-else-if="inputData.col.name === 'is_extra_mark'">
+            <q-toggle v-model="loadedQuestions[inputData.props.row.id].is_extra_mark"
+                      :disable="!loadedQuestions[inputData.props.row.id].selected"
+                      @update:model-value="emitValues" />
+          </template>
+          <template v-else-if="inputData.col.name === 'action'">
+            <div class="action-column-entity-index">
+              <q-btn color="primary"
+                     outline
+                     :to="{ name: 'Admin.Unit.Questions.Show', params: { unit_id: $route.params.unit_id, session_template_id: selectedSessionId, id: inputData.props.row.id } }">
+                مشاهده پاسخ
+                <q-icon name="visibility"
+                        class="q-ml-md" />
+              </q-btn>
+            </div>
+          </template>
+          <template v-else>
+            {{ inputData.col.value }}
+          </template>
+        </template>
+      </entity-index>
+    </div>
+    <div class="content-list q-mt-lg">
+      <div class="row">
+        <div class="col-md-4 col-12">
+          تعداد سوالات اجباری انتخاب شده:
+          {{ countOfForceQuestions }}
+        </div>
+        <div class="col-md-4 col-12">
+          مجموع بارم‌ها:
+          {{ sumOfTotalMarks }}
+        </div>
+        <div class="col-12" />
+        <div class="col-md-4 col-12">
+          تعداد سوالات اختیاری انتخاب شده:
+          {{ countOfExtraQuestions }}
+        </div>
+        <div class="col-md-4 col-12">
+          مجموع بارم سوالات اختیاری:
+          {{ sumOfExtraMarks }}
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { EntityIndex } from 'quasar-crud'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import NormalizeNumber from 'src/assets/js/NormalizeNumber.js'
+import { Question, QuestionList } from 'src/models/Question.js'
+import { SessionTemplateList } from 'src/models/SessionTemplate.js'
+
+export default {
+  name: 'QuestionsSelector',
+  components: {
+    EntityIndex
+  },
+  props: {
+    value: {
+      default: () => [],
+      type: [Array, String, Number, Boolean]
+    },
+    readonly: {
+      default: false,
+      type: Boolean
+    },
+    showPageLink: {
+      default: true,
+      type: Boolean
+    },
+    showDownloadLink: {
+      default: false,
+      type: Boolean
+    },
+    unitId: {
+      default: null,
+      type: [Number, String]
+    }
+  },
+  emits: ['update:value'],
+  data () {
+    return {
+      selectedSession: null,
+      sessions: new SessionTemplateList(),
+      sessionQuestions: new QuestionList(),
+      loadedQuestions: {},
+
+      api: APIGateway.question.APIAdresses.base,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [
+        { type: 'hidden', name: 'type', value: 'QUESTION_BANK' },
+        { type: 'hidden', name: 'session_template_questions__session_template', value: null }
+      ],
+      table: {
+        columns: [
+          {
+            name: 'choice',
+            required: true,
+            label: 'انتخاب',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'id',
+            required: true,
+            label: 'شناسه',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'text',
+            required: true,
+            label: 'عنوان سوال',
+            align: 'left',
+            field: row => row.text
+          },
+          {
+            name: 'mark',
+            required: true,
+            label: 'بارم',
+            align: 'left',
+            field: row => row.mark
+          },
+          {
+            name: 'level',
+            required: true,
+            label: 'سطح سوال',
+            align: 'left',
+            field: row => new Question(row).level_info.label
+          },
+          {
+            name: 'is_extra_mark',
+            required: true,
+            label: 'سوال امتیازی',
+            align: 'left',
+            field: row => ''
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'عملیات',
+            align: 'left',
+            field: row => ''
+          }
+        ]
+      }
+    }
+  },
+  computed: {
+    countOfForceQuestions () {
+      return this.selectedQuestions.filter(item => !item.is_extra_mark).length
+    },
+    countOfExtraQuestions () {
+      return this.selectedQuestions.filter(item => item.is_extra_mark).length
+    },
+    sumOfExtraMarks () {
+      let sum = 0
+      this.selectedQuestions.filter(item => item.is_extra_mark).forEach(item => {
+        sum += parseInt(NormalizeNumber.toEnglish(item.mark.toString()))
+      })
+
+      return sum
+    },
+    sumOfTotalMarks () {
+      let sum = 0
+      this.selectedQuestions.forEach(item => {
+        sum += parseInt(NormalizeNumber.toEnglish(item.mark.toString()))
+      })
+
+      return sum
+    },
+    selectedQuestions () {
+      const resultArray = []
+      Object.keys(this.loadedQuestions).forEach(questionId => {
+        if (this.loadedQuestions[questionId].selected) {
+          resultArray.push(this.loadedQuestions[questionId])
+        }
+      })
+      return resultArray
+    },
+    localValue: {
+      get () {
+        return this.value
+      },
+      set (newValue) {
+        this.$emit('update:value', newValue)
+      }
+    },
+    sessionOptions () {
+      return this.sessions.list.map(item => {
+        return {
+          value: item,
+          label: item.title
+        }
+      })
+    },
+    selectedSessionId () {
+      if (!this.selectedSession) {
+        return null
+      }
+      return this.selectedSession.id
+    }
+  },
+  watch: {
+    value (newValue) {
+      this.updateloadedQuestionsByArray(newValue, true)
+    }
+  },
+  mounted() {
+    this.getSessions()
+  },
+  methods: {
+    emitValues () {
+      this.localValue = this.selectedQuestions
+    },
+    filterQuestionList () {
+      if (!this.selectedSessionId) {
+        return
+      }
+      FormBuilderAssist.setAttributeByName(this.inputs, 'session_template_questions__session_template', 'value', this.selectedSessionId)
+      this.$refs.entityIndexQuestions.search()
+    },
+    getSessions () {
+      this.sessions.loading = true
+      APIGateway.sessionTemplate.index({ unit: this.unitId, per_page: 999999 })
+        .then((data) => {
+          this.sessions = new SessionTemplateList(data.list)
+          if (this.sessions.list.length > 0) {
+            this.selectedSession = this.sessions.list[0]
+            this.filterQuestionList()
+          }
+          this.sessions.loading = false
+        })
+        .catch(() => {
+          this.sessions.loading = false
+        })
+    },
+    getSessionQuestions () {
+      this.sessions.loading = true
+      APIGateway.sessionTemplate.index({ unit: this.unitId, per_page: 999999 })
+        .then((data) => {
+          this.sessions = new SessionTemplateList(data.list)
+          if (this.sessionOptions.length > 0) {
+            this.selectedSession = this.sessionOptions[0]
+          }
+          this.sessions.loading = false
+        })
+        .catch(() => {
+          this.sessions.loading = false
+        })
+    },
+    onChangeQuestionListPage (response) {
+      this.updateloadedQuestionsByArray(response.data.results)
+    },
+    updateloadedQuestionsByArray (array, allSelected = false) {
+      array.forEach(item => {
+        const questionId = item.id || item.question
+        const mark = item.mark || 0
+        const isExtraMark = item.is_extra_mark || false
+        const selected = allSelected
+        const target = this.loadedQuestions[questionId]
+        if (!target) {
+          this.loadedQuestions[questionId] = {
+            question: questionId,
+            mark,
+            is_extra_mark: isExtraMark,
+            selected
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.ContentsSelector {
+}
+</style>

+ 205 - 0
src/components/FormBuilderCustumComponents/QuestionsSelector/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.loading" />
+    </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.loading = false
+        })
+        .catch(() => {
+          this.contentOptions.loading = false
+        })
+    },
+    onChangeBakhsh () {
+      // this.localContents = []
+    }
+  }
+
+}
+</script>
+
+<style lang="scss" scoped>
+.ContentCategorySelector {
+}
+</style>

+ 1 - 1
src/components/Widgets/SessionTemplate/AdminSessionTemplateQuestionCreate/AdminSessionTemplateQuestionCreate.vue

@@ -79,7 +79,7 @@ export default {
       this.entityLoading = true
       this.$refs.entityEdit.editEntity(true)
         .then((response) => {
-          this.$router.push({ name: 'Admin.Unit.Questions.Show', params: { session_template_id: this.$route.params.session_template_id, id: response.data.id } })
+          this.$router.push({ name: 'Admin.Unit.Questions.Show', params: { unit_id: this.$route.params.unit_id, session_template_id: this.$route.params.session_template_id, id: response.data.id } })
           this.entityLoading = false
         })
         .catch(() => {

+ 3 - 3
src/components/Widgets/SessionTemplate/AdminSessionTemplateQuestionList/AdminSessionTemplateQuestionList.vue

@@ -10,7 +10,7 @@
       <q-btn label="ایجاد سوال جدید"
              color="primary"
              class="q-mr-md"
-             :to="{ name: 'Admin.Unit.Questions.Create', pragma: { session_template_id: $route.params.session_template_id } }" />
+             :to="{ name: 'Admin.Unit.Questions.Create', pragma: { unit_id: $route.params.unit_id, session_template_id: $route.params.session_template_id } }" />
       <q-btn flat
              color="grey"
              @click="$router.go(-1)">
@@ -20,7 +20,7 @@
     </div>
     <entity-index v-if="mounted"
                   v-model:value="inputs"
-                  title="لیست سوالات جلسه افتتاحیه"
+                  title="لیست سوالات"
                   :api="api"
                   :table="table"
                   :table-keys="tableKeys"
@@ -38,7 +38,7 @@
                    outline
                    round
                    icon="drive_file_rename_outline"
-                   :to="{ name: 'Admin.Unit.Questions.Show', params: { session_template_id: this.$route.params.session_template_id, id: inputData.props.row.id } }"
+                   :to="{ name: 'Admin.Unit.Questions.Show', params: { unit_id: this.$route.params.unit_id, session_template_id: this.$route.params.session_template_id, id: inputData.props.row.id } }"
                    class="q-mr-md" />
             <delete-btn @click="showConfirmRemoveDialog(inputData.props.row, 'id', getRemoveMessage(inputData.props.row))" />
           </div>

+ 104 - 0
src/components/Widgets/Test/AdminTestCreate/AdminTestCreate.vue

@@ -0,0 +1,104 @@
+<template>
+  <div class="AdminTestCreate"
+       :style="localOptions.style">
+    <div class="flex justify-end">
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-create v-if="mounted"
+                   ref="entityCreate"
+                   v-model:value="inputs"
+                   title="مشخصات آزمون"
+                   :api="api"
+                   :entity-id-key="entityIdKey"
+                   :entity-param-key="entityParamKey"
+                   :show-route-name="showRouteName"
+                   :show-close-button="false"
+                   :show-edit-button="false"
+                   :show-expand-button="false"
+                   :show-save-button="false"
+                   :show-reload-button="false" />
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityCreate } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import BtnControl from 'src/components/Control/btn.vue'
+import QuestionsSelector from 'src/components/FormBuilderCustumComponents/QuestionsSelector/QuestionsSelector.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const QuestionsSelectorComp = shallowRef(QuestionsSelector)
+
+export default {
+  name: 'AdminTestCreate',
+  components: { EntityCreate },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      mounted: false,
+      entityLoading: false,
+      api: APIGateway.test.APIAdresses.base,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Unit.Test.Show',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان آزمون', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات آزمون', col: 'col-md-12 col-12' },
+        { type: 'hidden', name: 'unit', responseKey: 'unit', value: this.$route.params.unit_id },
+        { type: QuestionsSelectorComp, name: 'test_questions', responseKey: 'test_questions', unitId: this.$route.params.unit_id, col: 'col-md-12 col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'تایید نهایی', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' }
+      ]
+    }
+  },
+  mounted() {
+    this.setActionBtn()
+    this.mounted = true
+  },
+  methods: {
+    setActionBtn () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.create)
+    },
+    create() {
+      this.entityLoading = true
+      this.$refs.entityCreate.createEntity()
+        .then((response) => {
+          this.$router.push({ name: 'Admin.Unit.Test.Show', params: { unit_id: this.$route.params.unit_id, id: response.data.id } })
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminTestCreate {
+  .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/Test/AdminTestCreate/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/Test/AdminTestShow/AdminTestShow.vue

@@ -0,0 +1,104 @@
+<template>
+  <div class="AdminTestShow"
+       :style="localOptions.style">
+    <div class="flex justify-end">
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-edit v-if="mounted"
+                 ref="entityEdit"
+                 v-model:value="inputs"
+                 title="مشخصات آزمون"
+                 :api="api"
+                 :entity-id-key="entityIdKey"
+                 :entity-param-key="entityParamKey"
+                 :show-close-button="false"
+                 :show-edit-button="false"
+                 :show-expand-button="false"
+                 :show-save-button="false"
+                 :show-reload-button="false"
+                 :redirect-after-edit="false" />
+  </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 { FormBuilderAssist } from 'quasar-form-builder'
+import BtnControl from 'src/components/Control/btn.vue'
+import QuestionsSelector from 'src/components/FormBuilderCustumComponents/QuestionsSelector/QuestionsSelector.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const QuestionsSelectorComp = shallowRef(QuestionsSelector)
+
+export default {
+  name: 'AdminTestShow',
+  components: { EntityEdit },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      mounted: false,
+      entityLoading: false,
+      api: APIGateway.test.APIAdresses.byId(this.$route.params.id),
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان آزمون', placeholder: ' ', col: 'col-md-12 col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات آزمون', col: 'col-md-12 col-12' },
+        { type: 'hidden', name: 'id', responseKey: 'id' },
+        { type: 'hidden', name: 'unit', responseKey: 'unit' },
+        { type: QuestionsSelectorComp, name: 'test_questions', responseKey: 'test_questions', unitId: this.$route.params.unit_id, col: 'col-md-12 col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'تایید نهایی', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' }
+      ]
+    }
+  },
+  mounted() {
+    this.setActionBtn()
+    this.mounted = true
+  },
+  methods: {
+    setActionBtn () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.edit)
+    },
+    edit() {
+      this.entityLoading = true
+      this.$refs.entityEdit.editEntity()
+        .then(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminTestShow {
+  .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/Test/AdminTestShow/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>

+ 52 - 0
src/models/Test.js

@@ -0,0 +1,52 @@
+import { Collection, Model } from 'js-abstract-model'
+
+class Test extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      { key: 'title' },
+      { key: 'description' },
+      { key: 'level' },
+      { key: 'unit' },
+      {
+        key: 'test_questions',
+        default: []
+      }
+    ], {
+      level: {
+        infoKey: 'level_info',
+        enumListKey: 'levelEnums',
+        enums: [
+          {
+            label: 'سطح یک',
+            value: 'LEVEL1'
+          },
+          {
+            label: 'سطح دو',
+            value: 'LEVEL2'
+          },
+          {
+            label: 'سطح سه',
+            value: 'LEVEL3'
+          },
+          {
+            label: 'سطح چهار',
+            value: 'LEVEL4'
+          },
+          {
+            label: 'سطح پنج',
+            value: 'LEVEL5'
+          }
+        ]
+      }
+    })
+  }
+}
+
+class TestList extends Collection {
+  model () {
+    return Test
+  }
+}
+
+export { Test, TestList }

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

@@ -0,0 +1,23 @@
+<template>
+  <q-page-builder v-model:sections="currenSections"
+                  v-model:options="pageConfig"
+                  :editable="pageBuilderEditable"
+                  :loading="pageBuilderLoading" />
+</template>
+
+<script>
+import { mixinPageOptions } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'Admin.Unit.Test.Create',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminTestCreate' }] }] }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

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

@@ -0,0 +1,23 @@
+<template>
+  <q-page-builder v-model:sections="currenSections"
+                  v-model:options="pageConfig"
+                  :editable="pageBuilderEditable"
+                  :loading="pageBuilderLoading" />
+</template>
+
+<script>
+import { mixinPageOptions } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'Admin.Unit.Test.Show',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminTestShow' }] }] }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 73 - 21
src/pages/Admin/Unit/show.vue

@@ -80,7 +80,7 @@
                          outline
                          label="بانک سوالات"
                          class="btn-go-to-question-bank"
-                         :to="{name: 'Admin.Unit.Questions.Index', params: {session_template_id: inputData.props.row.id}}" />
+                         :to="{name: 'Admin.Unit.Questions.Index', params: {unit_id: $route.params.id, session_template_id: inputData.props.row.id}}" />
                   <q-btn size="md"
                          color="primary"
                          label="تعیین جزییات"
@@ -98,12 +98,12 @@
       <q-tab-panel name="quiz"
                    class="q-pa-none">
         <entity-index v-if="mounted"
-                      ref="sessionEntityIndex"
-                      v-model:value="sessionFilterInputs"
+                      ref="testEntityIndex"
+                      v-model:value="testFilterInputs"
                       title="لیست آزمون ها"
-                      :api="sessionApi"
-                      :table="sessionTable"
-                      :table-keys="sessionTableKeys"
+                      :api="testApi"
+                      :table="testTable"
+                      :table-keys="testTableKeys"
                       :show-reload-button="false"
                       :show-search-button="false"
                       :show-expand-button="false">
@@ -113,23 +113,17 @@
                      outline
                      label="ایجاد آزمون جدید"
                      :loading="newSessionLoading"
-                     @click="createSession" />
+                     :to="{name: 'Admin.Unit.Test.Create', params: {unit_id: $route.params.id}}" />
             </div>
           </template>
           <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.Unit.Questions.Index', params: {session_template_id: inputData.props.row.id}}" />
                   <q-btn size="md"
                          color="primary"
                          label="تعیین جزییات"
-                         :to="{name: 'Admin.SessionTemplate.Show', params: {id: inputData.props.row.id}}" />
+                         :to="{name: 'Admin.Unit.Test.Show', params: {unit_id: $route.params.id, id: inputData.props.row.id}}" />
                   <delete-btn @click="showConfirmRemoveDialog(inputData.props.row, 'id', getRemoveMessage(inputData.props.row))" />
                 </div>
               </div>
@@ -169,7 +163,7 @@ export default {
       newSessionLoading: false,
       newSessionName: '-',
       newSessionSessionCount: null,
-      api: null,
+      api: APIGateway.unit.APIAdresses.byId(this.$route.params.id),
       entityIdKey: 'id',
       entityParamKey: 'id',
       showRouteName: 'Admin.Unit.Show',
@@ -178,7 +172,7 @@ export default {
         { type: 'inputEditor', name: 'rules', responseKey: 'rules', label: 'قوانین درس', col: 'col-12' },
         { type: PostRequisitesComp, name: 'prerequisites', responseKey: 'prerequisites', col: 'col-12' },
         { type: 'hidden', name: 'id', responseKey: 'id', label: 'id', col: 'col-md-3 col-12' },
-        { type: 'hidden', name: 'category', responseKey: 'category_info.id', label: 'id', col: 'col-md-3 col-12' }
+        { type: 'hidden', name: 'category', responseKey: 'category_info.id' }
       ],
 
       categories: new UnitCategoryList(),
@@ -188,9 +182,7 @@ export default {
         unit: null
       },
 
-      sessionFilterInputs: [],
-
-      sessionApi: null,
+      sessionApi: APIGateway.sessionTemplate.APIAdresses.base,
       sessionTable: {
         columns: [
           {
@@ -238,18 +230,78 @@ export default {
         ],
         data: []
       },
+      sessionFilterInputs: [
+        { type: 'hidden', name: 'unit', value: this.$route.params.id }
+      ],
       sessionTableKeys: {
         data: 'results',
         total: 'count',
         currentPage: 'current',
         perPage: 'per_page',
         pageKey: 'page'
+      },
+
+      testApi: APIGateway.test.APIAdresses.base,
+      testTable: {
+        columns: [
+          {
+            name: 'id',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'عنوان آزمون',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'count',
+            required: true,
+            label: 'تعداد سوالات',
+            align: 'left',
+            field: () => '...'
+          },
+          {
+            name: 'level',
+            required: true,
+            label: 'درجه سختی',
+            align: 'left',
+            field: row => row.level
+          },
+          {
+            name: 'creation_time',
+            required: true,
+            label: 'تاریخ ایجاد',
+            align: 'left',
+            field: row => ShamsiDate.getDateTime(row.creation_time)
+          },
+          {
+            name: 'actions',
+            required: true,
+            label: 'عملیات',
+            align: 'left',
+            field: ''
+          }
+        ],
+        data: []
+      },
+      testFilterInputs: [
+        { type: 'hidden', name: 'unit', value: this.$route.params.id }
+      ],
+      testTableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
       }
     }
   },
   mounted () {
-    this.api = APIGateway.unit.APIAdresses.byId(this.$route.params.id)
-    this.sessionApi = APIGateway.sessionTemplate.APIAdresses.base + '?unit=' + this.$route.params.id
     // this.getCategories()
     this.mounted = true
   },

+ 10 - 1
src/router/routes.js

@@ -561,13 +561,22 @@ const routes = [
               { name: 'Admin.Unit.Show', path: ':id', component: () => import('src/pages/Admin/Unit/show.vue') },
               {
                 name: 'Admin.Unit.Questions',
-                path: ':session_template_id/questions',
+                path: ':unit_id/session_template/:session_template_id/questions',
                 component: () => import('src/layouts/bareLayout.vue'),
                 children: [
                   { name: 'Admin.Unit.Questions.Index', path: '', component: () => import('src/pages/Admin/Unit/QuestionsList.vue') },
                   { name: 'Admin.Unit.Questions.Create', path: 'create', component: () => import('src/pages/Admin/Unit/QuestionsCreate.vue') },
                   { name: 'Admin.Unit.Questions.Show', path: ':id', component: () => import('src/pages/Admin/Unit/QuestionsShow.vue') }
                 ]
+              },
+              {
+                name: 'Admin.Unit.Test',
+                path: ':unit_id/tests',
+                component: () => import('src/layouts/bareLayout.vue'),
+                children: [
+                  { name: 'Admin.Unit.Test.Show', path: ':id', component: () => import('src/pages/Admin/Test/Show.vue') },
+                  { name: 'Admin.Unit.Test.Create', path: 'create', component: () => import('src/pages/Admin/Test/Create.vue') }
+                ]
               }
             ]
           },