ali 2 yıl önce
ebeveyn
işleme
c24c0295ff

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

@@ -8,6 +8,7 @@ export default class RegistrationAPI extends APIRepository {
     this.APIAdresses = {
       base: '/lma/registrations',
       exportReport: '/lma/registrations/export_report',
+      examineeSheet: '/lma/registrations/examinee_sheet',
       classroomDetail: (registrationId) => '/lma/registrations/' + registrationId + '/classroom_detail',
       revertDroppedForCondition: (registrationId) => '/lma/registrations/' + registrationId + '/revert_dropped_for_condition',
       byId: (id) => '/lma/registrations/' + id

+ 3 - 3
src/api/models/Test.js

@@ -4,10 +4,10 @@ import { Test, TestList } from 'src/models/Test.js'
 
 export default class TestAPI extends APIRepository {
   constructor() {
-    super('test_sets', appApi)
+    super('tests', appApi)
     this.APIAdresses = {
-      base: '/lma/test_sets',
-      byId: (id) => '/lma/test_sets/' + id
+      base: '/lma/tests',
+      byId: (id) => '/lma/tests/' + id
     }
     this.CacheList = {
       base: this.name + this.APIAdresses.base

+ 4 - 1
src/assets/ShamsiDate.js

@@ -8,7 +8,10 @@ class ShamsiDate {
     return moment(dateTime, 'YYYY-M-D HH:mm:ss').format('jMMMM jYYYY')
   }
 
-  static getDateTime (dateTime) {
+  static getDateTime (dateTime, justDate = false) {
+    if (justDate) {
+      return moment(dateTime, 'YYYY-M-D HH:mm:ss').format('jYYYY/jMM/jDD')
+    }
     return moment(dateTime, 'YYYY-M-D HH:mm:ss').format('HH:mm:ss jYYYY/jMM/jDD')
   }
 

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

@@ -1,139 +0,0 @@
-<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>

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

@@ -1,70 +0,0 @@
-<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>

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

@@ -1,205 +0,0 @@
-<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>

+ 210 - 0
src/components/FormBuilderCustumComponents/UsersOfTestSelector/UsersOfTestSelector.vue

@@ -0,0 +1,210 @@
+<template>
+  <div class="UsersOfTestSelector q-mt-md">
+    <q-banner>
+      لیست کاربران
+    </q-banner>
+    <div class="content-list q-mt-lg">
+      <entity-index ref="entityIndexQuestions"
+                    v-model:value="inputs"
+                    :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="loadedItems[inputData.props.row.owner].selected"
+                        color="primary"
+                        @update:model-value="emitValues" />
+          </template>
+          <template v-else-if="inputData.col.name === 'number'">
+            {{ inputData.rowNumber }}
+          </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">
+          تعداد انتخاب شده:
+          {{ selectedItems.length }}
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { EntityIndex } from 'quasar-crud'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'UsersOfTestSelector',
+  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
+    },
+    classroomId: {
+      default: null,
+      type: [Number, String]
+    }
+  },
+  emits: ['update:value'],
+  data () {
+    return {
+      loadedItems: {},
+
+      api: APIGateway.registration.APIAdresses.examineeSheet,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [
+        { type: 'hidden', name: 'classroom', value: this.classroomId }
+      ],
+      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: 'owner_info.fullName',
+            required: true,
+            label: 'نام و نام خانوادگی',
+            align: 'left',
+            field: row => row.owner_info.firstname + ' ' + row.owner_info.lastname
+          },
+          {
+            name: 'owner_info.national_code',
+            required: true,
+            label: 'کد ملی',
+            align: 'left',
+            field: row => row.owner_info.national_code
+          },
+          {
+            name: 'owner_info.mobile_number',
+            required: true,
+            label: 'شماره تلفن',
+            align: 'left',
+            field: row => row.owner_info.mobile_number
+          },
+          {
+            name: 'is_passed_project_condition',
+            required: true,
+            label: 'پروژه',
+            align: 'left',
+            field: row => row.is_passed_project_condition ? 'مجاز' : 'غیر مجاز'
+          },
+          {
+            name: 'is_passed_assignment_condition',
+            required: true,
+            label: 'تکلیف',
+            align: 'left',
+            field: row => row.is_passed_assignment_condition ? 'مجاز' : 'غیر مجاز'
+          },
+          {
+            name: 'is_passed_attendance_condition',
+            required: true,
+            label: 'حضور و غیاب',
+            align: 'left',
+            field: row => row.is_passed_attendance_condition ? 'مجاز' : 'غیر مجاز'
+          }
+        ]
+      }
+    }
+  },
+  computed: {
+    selectedItems () {
+      const resultArray = []
+      Object.keys(this.loadedItems).forEach(itemId => {
+        if (this.loadedItems[itemId].selected) {
+          resultArray.push(this.loadedItems[itemId])
+        }
+      })
+      return resultArray
+    },
+    localValue: {
+      get () {
+        return this.value
+      },
+      set (newValue) {
+        this.$emit('update:value', newValue)
+      }
+    }
+  },
+  watch: {
+    value (newValue) {
+      this.updateloadedItemsByArray(newValue, true)
+    }
+  },
+  methods: {
+    emitValues () {
+      this.localValue = this.selectedItems.map(item => item.id)
+    },
+    onChangeQuestionListPage (response) {
+      this.updateloadedItemsByArray(response.data.results)
+    },
+    updateloadedItemsByArray (array, allSelected = false) {
+      array.forEach(item => {
+        const itemId = item.owner || item.id || item
+        const selected = allSelected || this.value.includes(itemId)
+        const target = this.loadedItems[itemId]
+        if (!target) {
+          this.loadedItems[itemId] = {
+            id: itemId,
+            selected
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.UsersOfTestSelector {
+}
+</style>

+ 0 - 1
src/components/Widgets/Classroom/AdminClassroomShow/AdminClassroomShow.vue

@@ -76,7 +76,6 @@
       <q-tab-panel name="exams"
                    class="q-pa-none">
         <test-list :classroom-id="$route.params.id"
-                   :classroom="classroom"
                    :classroom-type="localOptions.classroomType" />
       </q-tab-panel>
       <q-tab-panel name="members"

+ 8 - 23
src/components/Widgets/Classroom/AdminClassroomShow/components/TestList.vue

@@ -1,5 +1,5 @@
 <template>
-  <entity-index v-if="classroomLoaded"
+  <entity-index v-if="mounted"
                 ref="testList"
                 v-model:value="testListInputs"
                 :title="listTitle"
@@ -13,7 +13,7 @@
       <q-btn color="primary"
              outline
              :loading="addNewSessionLoading"
-             @click="addNewSession">
+             :to="{name: 'Admin.Classroom.TestSet.Index', params: {classroom_id: classroomId}}">
         {{ addBtnTitle }}
       </q-btn>
     </template>
@@ -51,9 +51,7 @@
 import { EntityIndex } from 'quasar-crud'
 import ShamsiDate from 'src/assets/ShamsiDate.js'
 import { APIGateway } from 'src/api/APIGateway.js'
-import { Classroom } from 'src/models/Classroom.js'
 import DeleteBtn from 'src/components/Control/DeleteBtn.vue'
-import { FormBuilderAssist } from 'quasar-form-builder'
 
 export default {
   name: 'TestList',
@@ -62,9 +60,9 @@ export default {
     EntityIndex
   },
   props: {
-    classroom: {
-      type: Classroom,
-      default: new Classroom()
+    classroomId: {
+      type: Number,
+      default: null
     },
     classroomType: {
       type: String,
@@ -73,13 +71,13 @@ export default {
   },
   data () {
     return {
-      classroomLoaded: false,
+      mounted: false,
       listTitle: 'لیست آزمون ها',
       addBtnTitle: 'افزودن آزمون',
       addNewSessionLoading: false,
 
       testListInputs: [
-        { type: 'hidden', name: 'unit', value: null }
+        { type: 'hidden', name: 'classroom', value: this.classroomId }
       ],
       testListApi: APIGateway.session.APIAdresses.base,
       testListTable: {
@@ -138,9 +136,6 @@ export default {
     }
   },
   computed: {
-    classroomId () {
-      return this.classroom.id
-    },
     sessionAttendanceSheetList () {
       if (this.classroomType === 'TRAINING') {
         return 'Admin.Classroom.Session.AttendanceSheetList'
@@ -151,19 +146,9 @@ export default {
       return 'Admin.Classroom.Session.AttendanceSheetList'
     }
   },
-  watch: {
-    classroomId (newValue) {
-      if (!newValue) {
-        return
-      }
-
-      const unitId = newValue.unit || newValue.unit_info.id
-      FormBuilderAssist.setAttributeByName(this.testListInputs, 'unit', 'value', unitId)
-      this.classroomLoaded = true
-    }
-  },
   mounted () {
     this.setClassroomTypeOfInputs()
+    this.mounted = true
   },
   methods: {
     setClassroomTypeOfInputs () {

+ 16 - 9
src/components/Widgets/Test/AdminTestCreate/AdminTestCreate.vue

@@ -32,10 +32,10 @@ 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'
+import UsersOfTestSelector from 'src/components/FormBuilderCustumComponents/UsersOfTestSelector/UsersOfTestSelector.vue'
 
 const BtnControlComp = shallowRef(BtnControl)
-const QuestionsSelectorComp = shallowRef(QuestionsSelector)
+const UsersOfTestSelectorComp = shallowRef(UsersOfTestSelector)
 
 export default {
   name: 'AdminTestCreate',
@@ -50,11 +50,18 @@ export default {
       entityParamKey: 'id',
       showRouteName: 'Admin.Unit.TestSet.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' }
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-12' },
+        { type: 'inputEditor', name: 'detail', responseKey: 'detail', label: 'توضیحات آزمون', col: 'col-12' },
+        { type: 'dateTime', name: 'start_time', responseKey: 'start_time', label: 'زمان شروع آزمون', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'dateTime', name: 'end_time', responseKey: 'end_time', label: 'زمان پایان آزمون', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'input', name: 'duration_deadline', responseKey: 'duration_deadline', label: 'مدت زمان پاسخ دهی (دقیقه)', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'dateTime', name: 'announcement_time', responseKey: 'announcement_time', label: 'زمان اعلام نتایج', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'dateTime', name: 'objection_start_time', responseKey: 'objection_start_time', label: 'زمان شروع اعتراض', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'dateTime', name: 'objection_end_time', responseKey: 'objection_end_time', label: 'زمان پایان اعتراض', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'hidden', name: 'test_set', responseKey: 'test_set', value: this.$route.params.test_set_id },
+        { type: 'hidden', name: 'classroom', responseKey: 'classroom', value: this.$route.params.classroom_id },
+        { type: UsersOfTestSelectorComp, name: 'examinees', responseKey: 'examinees', classroomId: this.$route.params.classroom_id, col: 'col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'تعیین جزییات', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' }
       ]
     }
   },
@@ -68,9 +75,9 @@ export default {
     },
     create() {
       this.entityLoading = true
-      this.$refs.entityCreate.createEntity()
+      this.$refs.entityCreate.createEntity(false)
         .then((response) => {
-          this.$router.push({ name: 'Admin.Unit.TestSet.Show', params: { unit_id: this.$route.params.unit_id, id: response.data.id } })
+          this.$router.push({ name: 'Admin.Classroom.TestSet.Test.Show', params: { classroom_id: this.$route.params.classroom_id, test_set_id: this.$route.params.test_set_id, id: response.data.id } })
           this.entityLoading = false
         })
         .catch(() => {

+ 14 - 8
src/components/Widgets/Test/AdminTestShow/AdminTestShow.vue

@@ -32,10 +32,10 @@ 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'
+import UsersOfTestSelector from 'src/components/FormBuilderCustumComponents/UsersOfTestSelector/UsersOfTestSelector.vue'
 
 const BtnControlComp = shallowRef(BtnControl)
-const QuestionsSelectorComp = shallowRef(QuestionsSelector)
+const UsersOfTestSelectorComp = shallowRef(UsersOfTestSelector)
 
 export default {
   name: 'AdminTestShow',
@@ -49,12 +49,18 @@ export default {
       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' }
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-12' },
+        { type: 'inputEditor', name: 'detail', responseKey: 'detail', label: 'توضیحات آزمون', col: 'col-12' },
+        { type: 'dateTime', name: 'start_time', responseKey: 'start_time', label: 'زمان شروع آزمون', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'dateTime', name: 'end_time', responseKey: 'end_time', label: 'زمان پایان آزمون', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'input', name: 'duration_deadline', responseKey: 'duration_deadline', label: 'مدت زمان پاسخ دهی (دقیقه)', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'dateTime', name: 'announcement_time', responseKey: 'announcement_time', label: 'زمان اعلام نتایج', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'dateTime', name: 'objection_start_time', responseKey: 'objection_start_time', label: 'زمان شروع اعتراض', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'dateTime', name: 'objection_end_time', responseKey: 'objection_end_time', label: 'زمان پایان اعتراض', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'hidden', name: 'test_set', responseKey: 'test_set', value: this.$route.params.test_set_id },
+        { type: 'hidden', name: 'classroom', responseKey: 'classroom', value: this.$route.params.classroom_id },
+        { type: UsersOfTestSelectorComp, name: 'examinees', responseKey: 'examinees', classroomId: this.$route.params.classroom_id, col: 'col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'تعیین جزییات', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' }
       ]
     }
   },

+ 104 - 0
src/components/Widgets/TestSet/AdminTestSetCreate/AdminTestSetCreate.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.TestSet.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.TestSet.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/TestSet/AdminTestSetCreate/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>

+ 156 - 0
src/components/Widgets/TestSet/AdminTestSetIndex/AdminTestSetIndex.vue

@@ -0,0 +1,156 @@
+<template>
+  <div>
+    <div class="flex justify-end">
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-index v-if="mounted"
+                  ref="testSetList"
+                  v-model:value="testSetListInputs"
+                  title="لیست آزمون ها"
+                  :api="testSetListApi"
+                  :table="testSetListTable"
+                  :table-keys="testSetListTableKeys"
+                  :show-search-button="false"
+                  :show-reload-button="false"
+                  :show-expand-button="false">
+      <template v-slot:entity-index-table-cell="{inputData}">
+        <template v-if="inputData.col.name === 'number'">
+          {{ inputData.rowNumber }}
+        </template>
+        <template v-else-if="inputData.col.name === 'actions'">
+          <div class="action-column-entity-index">
+            <q-btn size="md"
+                   color="primary"
+                   label="انتخاب"
+                   :to="{name: 'Admin.Classroom.TestSet.Test.Create', params: {classroom_id: $route.params.classroom_id, test_set_id:inputData.props.row.id}}"
+                   :loading="createInvoiceLoading"
+                   class="q-mr-md" />
+          </div>
+        </template>
+        <template v-else>
+          {{ inputData.col.value }}
+        </template>
+      </template>
+    </entity-index>
+    <q-skeleton v-else
+                type="rect"
+                height="200px" />
+  </div>
+</template>
+
+<script>
+import { EntityIndex } from 'quasar-crud'
+import { TestSet } from 'src/models/TestSet.js'
+import ShamsiDate from 'src/assets/ShamsiDate.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { Classroom } from 'src/models/Classroom.js'
+import { mixinAuthData } from 'src/mixin/Mixins.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+
+export default {
+  name: 'LeaderTeamsShow',
+  components: {
+    EntityIndex
+  },
+  mixins: [mixinAuthData],
+  data () {
+    return {
+      mounted: false,
+      createInvoiceLoading: false,
+      classroom: new Classroom(),
+
+      dropClassroomByAdminLoading: false,
+      exportReportLoading: false,
+      testSetListInputs: [
+        { type: 'hidden', name: 'classroom', value: this.$route.params.classroom_id }
+      ],
+      testSetListApi: APIGateway.testSet.APIAdresses.base,
+      testSetListTable: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'id',
+            required: true,
+            label: 'شناسه',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'عنوان آزمون',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'test_set_questions.length',
+            required: true,
+            label: 'تعداد سوالات',
+            align: 'left',
+            field: row => row.test_set_questions.length
+          },
+          {
+            name: 'level',
+            required: true,
+            label: 'درجه سختی',
+            align: 'left',
+            field: row => (new TestSet(row)).level_info.label
+          },
+          {
+            name: 'beginning_time',
+            required: true,
+            label: 'تاریخ ایجاد',
+            align: 'left',
+            field: row => row.creation_time ? ShamsiDate.getDateTime(row.creation_time) : '-'
+          },
+          {
+            name: 'creator_info',
+            required: true,
+            label: 'ایجاد کننده',
+            align: 'left',
+            field: row => row.creator_info.firstname + ' ' + row.creator_info.lastname
+          },
+          {
+            name: 'actions',
+            required: true,
+            label: 'انتخاب',
+            align: 'left',
+            field: ''
+          }
+        ]
+      },
+      testSetListTableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      }
+    }
+  },
+  mounted () {
+    this.setMembersListActionBtn()
+    FormBuilderAssist.setAttributeByName(this.testSetListInputs, 'leader', 'value', this.user.id)
+    this.mounted = true
+  },
+  methods: {
+    setMembersListActionBtn () {
+      FormBuilderAssist.setAttributeByName(this.testSetListInputs, 'btn', 'atClick', this.searchMembersList)
+    },
+    searchMembersList () {
+      this.$refs.testSetList.search()
+    }
+  }
+}
+</script>

+ 21 - 0
src/components/Widgets/TestSet/AdminTestSetIndex/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/TestSet/AdminTestSetShow/AdminTestSetShow.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/TestSet/AdminTestSetShow/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>

+ 23 - 0
src/pages/Admin/TestSet/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.TestSet.Create',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminTestSetCreate' }] }] }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 23 - 0
src/pages/Admin/TestSet/Index.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.TestSet.Show',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminTestSetIndex' }] }] }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 23 - 0
src/pages/Admin/TestSet/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.TestSet.Show',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminTestSetShow' }] }] }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 20 - 3
src/router/routes.js

@@ -574,8 +574,8 @@ const routes = [
                 path: ':unit_id/test_set',
                 component: () => import('src/layouts/bareLayout.vue'),
                 children: [
-                  { name: 'Admin.Unit.TestSet.Show', path: ':id', component: () => import('src/pages/Admin/Test/Show.vue') },
-                  { name: 'Admin.Unit.TestSet.Create', path: 'create', component: () => import('src/pages/Admin/Test/Create.vue') }
+                  { name: 'Admin.Unit.TestSet.Show', path: ':id', component: () => import('src/pages/Admin/TestSet/Show.vue') },
+                  { name: 'Admin.Unit.TestSet.Create', path: 'create', component: () => import('src/pages/Admin/TestSet/Create.vue') }
                 ]
               }
             ]
@@ -635,7 +635,24 @@ const routes = [
               { name: 'Admin.Classroom.projectAttendanceSheets.Show', path: ':classroom_id/project/:project_id/projectAttendanceSheet/:project_attendance_sheets_id', component: () => import('src/pages/Admin/Classroom/ProjectAttendanceSheet/show.vue') },
               { name: 'Admin.Classroom.Team.Create', path: ':classroom_id/team/create', component: () => import('src/pages/Admin/Classroom/Team/create.vue') },
               { name: 'Admin.Classroom.Team.Show', path: ':classroom_id/team/:team_id', component: () => import('src/pages/Admin/Classroom/Team/show.vue') },
-              { name: 'Admin.Classroom.Ticket', path: 'ticket', component: () => import('src/pages/Admin/Classroom/Ticket.vue'), breadcrumbs: { title: 'درخواست ها دوره های آموزشی' } }
+              { name: 'Admin.Classroom.Ticket', path: 'ticket', component: () => import('src/pages/Admin/Classroom/Ticket.vue'), breadcrumbs: { title: 'درخواست ها دوره های آموزشی' } },
+              {
+                name: 'Admin.Classroom.TestSet',
+                path: ':classroom_id',
+                component: () => import('src/layouts/bareLayout.vue'),
+                children: [
+                  {
+                    name: 'Admin.Classroom.TestSet.Index',
+                    path: 'test_set',
+                    component: () => import('src/layouts/bareLayout.vue'),
+                    children: [
+                      { name: 'Admin.Classroom.TestSet.Index', path: '', component: () => import('src/pages/Admin/TestSet/Index.vue') },
+                      { name: 'Admin.Classroom.TestSet.Test.Show', path: ':test_set_id/test/:id', component: () => import('src/pages/Admin/Test/Show.vue') },
+                      { name: 'Admin.Classroom.TestSet.Test.Create', path: ':test_set_id/test/create', component: () => import('src/pages/Admin/Test/Create.vue') }
+                    ]
+                  }
+                ]
+              }
             ]
           },
           {