Explorar el Código

see also #257

ali hace 2 años
padre
commit
96c3770188
Se han modificado 23 ficheros con 1565 adiciones y 13 borrados
  1. 2 2
      package.json
  2. 15 0
      src/api/models/AnswerBook.js
  3. 17 0
      src/api/models/AnswerSheet.js
  4. 1 0
      src/api/models/Classroom.js
  5. 8 0
      src/components/Template/SideBard/UserPanelSideBar.vue
  6. 1 1
      src/components/Widgets/Classroom/AdminClassroomShow/components/TestList.vue
  7. 253 0
      src/components/Widgets/Classroom/ClassroomAnswerBookList/ClassroomAnswerBookList.vue
  8. 28 0
      src/components/Widgets/Classroom/ClassroomAnswerBookList/OptionPanel.vue
  9. 235 0
      src/components/Widgets/Classroom/ClassroomAnswerBookSubmitGrade/ClassroomAnswerBookSubmitGrade.vue
  10. 28 0
      src/components/Widgets/Classroom/ClassroomAnswerBookSubmitGrade/OptionPanel.vue
  11. 235 0
      src/components/Widgets/Classroom/MyAsGraderClassroomList/MyAsGraderClassroomList.vue
  12. 28 0
      src/components/Widgets/Classroom/MyAsGraderClassroomList/OptionPanel.vue
  13. 288 0
      src/components/Widgets/Test/AdminSendTestGrade/AdminSendTestGrade.vue
  14. 21 0
      src/components/Widgets/Test/AdminSendTestGrade/OptionPanel.vue
  15. 267 0
      src/components/Widgets/Test/AdminShowTestGrade/AdminShowTestGrade.vue
  16. 21 0
      src/components/Widgets/Test/AdminShowTestGrade/OptionPanel.vue
  17. 4 0
      src/models/AnswerBook.js
  18. 23 0
      src/pages/Admin/Classroom/ClassroomAnswerBookShowGrade.vue
  19. 23 0
      src/pages/Admin/Classroom/ClassroomAnswerBookSubmitGrade.vue
  20. 23 0
      src/pages/Admin/Classroom/ClassroomAnswerBooks.vue
  21. 23 0
      src/pages/Admin/Classroom/MyAsGraderClassrooms.vue
  22. 12 1
      src/router/routes.js
  23. 9 9
      yarn.lock

+ 2 - 2
package.json

@@ -51,8 +51,8 @@
     "process": "^0.11.10",
     "quasar": "^2.13.0",
     "quasar-app-extension-q-page-builder": "^0.1.71",
-    "quasar-crud": "^0.3.55",
-    "quasar-form-builder": "^0.1.110",
+    "quasar-crud": "^0.4.0",
+    "quasar-form-builder": "^0.2.0",
     "quasar-template-builder": "^1.0.23",
     "quasar-ui-q-page-builder": "^0.1.71",
     "socket.io-client": "^4.6.1",

+ 15 - 0
src/api/models/AnswerBook.js

@@ -9,6 +9,7 @@ export default class AnswerBookAPI extends APIRepository {
       base: '/lma/answer_books',
       byId: (id) => '/lma/answer_books/' + id,
       confirmAnswers: (id) => '/lma/answer_books/' + id + '/confirm_answers',
+      setGraderToMe: (id) => '/lma/answer_books/' + id + '/set_grader_to_me',
       submitObjectionRequest: (id) => '/lma/answer_books/' + id + '/submit_objection_request',
       getTestQuestions: (id) => '/lma/answer_books/' + id + '/get_test_questions',
       submitOverallAnswer: (id) => '/lma/answer_books/' + id + '/submit_overall_answer'
@@ -103,4 +104,18 @@ export default class AnswerBookAPI extends APIRepository {
       }
     })
   }
+
+  setGraderToMe (answerBookId) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.setGraderToMe(answerBookId),
+      resolveCallback: (response) => {
+        return new AnswerBook(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
 }

+ 17 - 0
src/api/models/AnswerSheet.js

@@ -87,4 +87,21 @@ export default class AnswerSheetAPI extends APIRepository {
       }
     })
   }
+
+  submitScore (answerSheetsId, score) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.byId(answerSheetsId),
+      data: {
+        score
+      },
+      resolveCallback: (response) => {
+        return new AnswerSheet(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
 }

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

@@ -9,6 +9,7 @@ export default class ClassroomAPI extends APIRepository {
     super('classrooms', appApi, '/lma/classrooms', Classroom)
     this.APIAdresses = {
       base: '/lma/classrooms',
+      myAsGraderClassrooms: '/lma/classrooms/my_as_a_grader_classrooms',
       byId: (id) => '/lma/classrooms/' + id,
       enroll: (id) => '/lma/classrooms/' + id + '/enroll',
       drop: (id) => '/lma/classrooms/' + id + '/drop',

+ 8 - 0
src/components/Template/SideBard/UserPanelSideBar.vue

@@ -69,6 +69,14 @@ export default {
             }
           ]
         },
+        {
+          title: 'تصحیح های من',
+          roles: ['superuser', 'grader'],
+          icon: 'quiz',
+          show: true,
+          active: false,
+          routeName: 'UserPanel.Classroom.MyAsGraderClassroom.Index'
+        },
         {
           title: 'رویدادها',
           routeName: null,

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

@@ -103,7 +103,7 @@ export default {
             field: row => row.title
           },
           {
-            name: 'title',
+            name: 'test_set_questions_length',
             required: true,
             label: 'تعداد سوالات',
             align: 'left',

+ 253 - 0
src/components/Widgets/Classroom/ClassroomAnswerBookList/ClassroomAnswerBookList.vue

@@ -0,0 +1,253 @@
+<template>
+  <div class="ClassroomAnswerBookList">
+    <entity-index v-if="mounted"
+                  ref="entityIndex"
+                  v-model:value="inputs"
+                  :title="'لیست ' + classroomTypeTitle"
+                  :api="api"
+                  :table="table"
+                  :table-keys="tableKeys"
+                  :show-expand-button="false"
+                  :show-reload-button="false"
+                  :show-search-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 v-if="inputData.props.row.status === 'SUBMITTED' || inputData.props.row.status === 'GRADING'"
+                   size="md"
+                   color="primary"
+                   label="تصحیح توسط من"
+                   :to="{name: 'UserPanel.Classroom.MyAsGraderClassroom.AnswerBooks.SubmitGrade', params: {classroom_id: $route.params.classroom_id, answer_book_id: inputData.props.row.id}}"
+                   class="q-mr-md" />
+            <q-btn size="md"
+                   color="primary"
+                   label="جزییات"
+                   :to="{name: 'UserPanel.Classroom.MyAsGraderClassroom.AnswerBooks.ShowGrade', params: {classroom_id: $route.params.classroom_id, answer_book_id: inputData.props.row.id}}"
+                   class="q-mr-md" />
+          </div>
+        </template>
+        <template v-else>
+          {{ inputData.col.value }}
+        </template>
+      </template>
+    </entity-index>
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityIndex } from 'quasar-crud'
+import { Test } from 'src/models/Test.js'
+import Enums from 'src/assets/Enums/Enums.js'
+// import ShamsiDate from 'src/assets/ShamsiDate.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import BtnControl from 'src/components/Control/btn.vue'
+import { UnitCategory } from 'src/models/UnitCategory.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+
+const BtnControlComp = shallowRef(BtnControl)
+
+export default {
+  name: 'ClassroomAnswerBookList',
+  components: {
+    EntityIndex
+  },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      mounted: false,
+      defaultOptions: {
+        classroomType: 'TRAINING'
+      },
+      api: APIGateway.answerBook.APIAdresses.base,
+      inputs: [
+        {
+          type: 'select',
+          name: 'status',
+          clearable: false,
+          options: [
+            { label: 'جاری', value: 'ONGOING' },
+            { label: 'در حال تصحیح', value: 'GRADING' },
+            { label: 'تصحیح شده', value: 'GRADED' }
+          ],
+          label: 'وضعیت',
+          placeholder: ' ',
+          value: 'ONGOING',
+          col: 'col-md-4 col-12'
+        },
+        { type: 'hidden', name: 'test__classroom', value: this.$route.params.classroom_id },
+        { type: BtnControlComp, name: 'btn', label: 'جستجو', placeholder: ' ', atClick: () => {}, col: 'col-md-2 col-12' }
+      ],
+      table: {
+        columns: [
+          {
+            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.mobile_number',
+            required: true,
+            label: 'موبایل',
+            align: 'left',
+            field: row => row.owner_info.mobile_number
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'عنوان آزمون',
+            align: 'left',
+            field: row => row.test_info.title
+          },
+          {
+            name: 'test_set_questions_length',
+            required: true,
+            label: 'تعداد سوالات',
+            align: 'left',
+            field: row => row.test_info.test_set_questions_length
+          },
+          // {
+          //   name: 'level',
+          //   required: true,
+          //   label: 'درچه سختی',
+          //   align: 'left',
+          //   field: row => (new Test(row.test_set_info)).level_info.label
+          // },
+          {
+            name: 'test_info_status_info',
+            required: true,
+            label: 'وضعیت آزمون',
+            align: 'left',
+            field: row => (new Test(row.test_info)).status_info.label
+          },
+          {
+            name: 'actions',
+            required: true,
+            label: 'عملیات',
+            align: 'left',
+            field: ''
+          }
+        ]
+      },
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      }
+    }
+  },
+  computed: {
+    classroomTypeTitle () {
+      const unitCategory = new UnitCategory({ type: this.localOptions.classroomType })
+      return unitCategory.type_info.label
+    },
+    createPageRouteName () {
+      if (this.localOptions.classroomType === 'TRAINING') {
+        return 'Admin.Classroom.Create'
+      }
+      if (this.localOptions.classroomType === 'EVENT') {
+        return 'Admin.Event.Create'
+      }
+      return 'Admin.Classroom.Create'
+    },
+    showPageRouteName () {
+      if (this.localOptions.classroomType === 'TRAINING') {
+        return 'Admin.Classroom.Show'
+      }
+      if (this.localOptions.classroomType === 'EVENT') {
+        return 'Admin.Event.Show'
+      }
+      return 'Admin.Classroom.Show'
+    }
+  },
+  mounted () {
+    this.setActionBtn()
+    this.setClassroomTypeOfInputs()
+    this.mounted = true
+  },
+  methods: {
+    setActionBtn () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.search)
+    },
+    setClassroomTypeOfInputs () {
+      this.table.columns.forEach(col => {
+        if (col.name === 'title') {
+          col.label = 'نام ' + this.classroomTypeTitle
+        }
+        if (col.name === 'professor_info') {
+          col.label = (this.localOptions.classroomType === 'TRAINING') ? 'استاد مربوطه' : 'برگزار کننده'
+        }
+      })
+    },
+    search () {
+      this.$refs.entityIndex.search()
+    },
+    getSelectOptions (result, value, label) {
+      return result.map(item => {
+        return {
+          value: item[value],
+          label: item[label]
+        }
+      })
+    },
+    getRemoveMessage (row) {
+      return 'آیا از حذف ' + row.title + ' اطمینان دارید؟'
+    },
+    getClassroomHoldingTypeTitle (type) {
+      const target = Enums.classroomHoldingTypes.find(item => item.value === type)
+      if (!target) {
+        return '-'
+      }
+
+      return target.label
+    },
+    getClassroomStatusesTitle (type) {
+      const target = Enums.classroomStatuses.find(item => item.value === type)
+      if (!target) {
+        return '-'
+      }
+
+      return target.label
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.ClassroomAnswerBookList {
+  .add-new-action-row {
+    display: flex;
+    flex-flow: row;
+    align-items: center;
+    justify-content: flex-end;
+    margin-bottom: 22px;
+  }
+  .action-column-entity-index {
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+  }
+}
+</style>

+ 28 - 0
src/components/Widgets/Classroom/ClassroomAnswerBookList/OptionPanel.vue

@@ -0,0 +1,28 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      <q-option-group v-model="localOptions.classroomType"
+                      :options="classroomTypeOptions" />
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { UnitCategory } from 'src/models/UnitCategory.js'
+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],
+  data () {
+    return {
+      classroomTypeOptions: (new UnitCategory()).typeEnums
+    }
+  }
+})
+</script>

+ 235 - 0
src/components/Widgets/Classroom/ClassroomAnswerBookSubmitGrade/ClassroomAnswerBookSubmitGrade.vue

@@ -0,0 +1,235 @@
+<template>
+  <div class="ClassroomAnswerBookList">
+    <entity-index v-if="mounted"
+                  ref="entityIndex"
+                  v-model:value="inputs"
+                  :title="'لیست ' + classroomTypeTitle"
+                  :api="api"
+                  :table="table"
+                  :table-keys="tableKeys"
+                  :show-expand-button="false"
+                  :show-reload-button="false"
+                  :show-search-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 === 'action'">
+          <div class="action-column-entity-index">
+            <q-btn size="md"
+                   color="primary"
+                   label="جزییات"
+                   :to="{name: 'UserPanel.Classroom.MyAsGraderClassroom.AnswerBooks', params: {classroom_id: inputData.props.row.id}}"
+                   class="q-mr-md" />
+          </div>
+        </template>
+        <template v-else>
+          {{ inputData.col.value }}
+        </template>
+      </template>
+    </entity-index>
+  </div>
+</template>
+
+<script>
+import { EntityIndex } from 'quasar-crud'
+import Enums from 'src/assets/Enums/Enums.js'
+import ShamsiDate from 'src/assets/ShamsiDate.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { UnitCategory } from 'src/models/UnitCategory.js'
+
+export default {
+  name: 'ClassroomAnswerBookList',
+  components: {
+    EntityIndex
+  },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      mounted: false,
+      inputs: [],
+      defaultOptions: {
+        classroomType: 'TRAINING'
+      },
+      api: APIGateway.classroom.APIAdresses.myAsGraderClassrooms,
+      table: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'id',
+            required: true,
+            label: 'شناسه',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'عنوان',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'audience_gender_type',
+            required: true,
+            label: 'جنسیت',
+            align: 'left',
+            field: row => (row.audience_gender_type === 'FEMALE') ? 'خواهران' : (row.audience_gender_type === 'MALE') ? 'برادران' : 'خواهران و برادران'
+          },
+          {
+            name: 'status',
+            required: true,
+            label: 'وضعیت',
+            align: 'left',
+            field: row => this.getClassroomStatusesTitle(row.status)
+          },
+          {
+            name: 'holding_type',
+            required: true,
+            label: 'نوع برگزاری',
+            align: 'left',
+            field: row => this.getClassroomHoldingTypeTitle(row.holding_type)
+          },
+          {
+            name: 'price',
+            required: true,
+            label: 'هزینه(ریال)',
+            align: 'left',
+            field: row => row.price
+          },
+          {
+            name: 'professor_info',
+            required: true,
+            label: 'استاد مربوطه',
+            align: 'left',
+            field: row => row.professor_info?.firstname + ' ' + row.professor_info?.lastname
+          },
+          {
+            name: 'beginning_registration_period',
+            required: true,
+            label: 'تاریخ و زمان ثبت نام',
+            align: 'left',
+            field: row => row.beginning_registration_period ? ShamsiDate.getDateTime(row.beginning_registration_period) : '-'
+          },
+          {
+            name: 'beginning_registration_period',
+            required: true,
+            label: 'ترم برگزاری',
+            align: 'left',
+            field: row => (row.beginning_registration_period) ? ShamsiDate.getTerm(row.beginning_registration_period) : '-'
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'عملیات',
+            align: 'left',
+            field: ''
+          }
+        ],
+        data: []
+      },
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      }
+    }
+  },
+  computed: {
+    classroomTypeTitle () {
+      const unitCategory = new UnitCategory({ type: this.localOptions.classroomType })
+      return unitCategory.type_info.label
+    },
+    createPageRouteName () {
+      if (this.localOptions.classroomType === 'TRAINING') {
+        return 'Admin.Classroom.Create'
+      }
+      if (this.localOptions.classroomType === 'EVENT') {
+        return 'Admin.Event.Create'
+      }
+      return 'Admin.Classroom.Create'
+    },
+    showPageRouteName () {
+      if (this.localOptions.classroomType === 'TRAINING') {
+        return 'Admin.Classroom.Show'
+      }
+      if (this.localOptions.classroomType === 'EVENT') {
+        return 'Admin.Event.Show'
+      }
+      return 'Admin.Classroom.Show'
+    }
+  },
+  mounted () {
+    this.setClassroomTypeOfInputs()
+    this.mounted = true
+  },
+  methods: {
+    setClassroomTypeOfInputs () {
+      this.table.columns.forEach(col => {
+        if (col.name === 'title') {
+          col.label = 'نام ' + this.classroomTypeTitle
+        }
+        if (col.name === 'professor_info') {
+          col.label = (this.localOptions.classroomType === 'TRAINING') ? 'استاد مربوطه' : 'برگزار کننده'
+        }
+      })
+    },
+    search () {
+      this.$refs.entityIndex.search()
+    },
+    getSelectOptions (result, value, label) {
+      return result.map(item => {
+        return {
+          value: item[value],
+          label: item[label]
+        }
+      })
+    },
+    getRemoveMessage (row) {
+      return 'آیا از حذف ' + row.title + ' اطمینان دارید؟'
+    },
+    getClassroomHoldingTypeTitle (type) {
+      const target = Enums.classroomHoldingTypes.find(item => item.value === type)
+      if (!target) {
+        return '-'
+      }
+
+      return target.label
+    },
+    getClassroomStatusesTitle (type) {
+      const target = Enums.classroomStatuses.find(item => item.value === type)
+      if (!target) {
+        return '-'
+      }
+
+      return target.label
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.ClassroomAnswerBookList {
+  .add-new-action-row {
+    display: flex;
+    flex-flow: row;
+    align-items: center;
+    justify-content: flex-end;
+    margin-bottom: 22px;
+  }
+  .action-column-entity-index {
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+  }
+}
+</style>

+ 28 - 0
src/components/Widgets/Classroom/ClassroomAnswerBookSubmitGrade/OptionPanel.vue

@@ -0,0 +1,28 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      <q-option-group v-model="localOptions.classroomType"
+                      :options="classroomTypeOptions" />
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { UnitCategory } from 'src/models/UnitCategory.js'
+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],
+  data () {
+    return {
+      classroomTypeOptions: (new UnitCategory()).typeEnums
+    }
+  }
+})
+</script>

+ 235 - 0
src/components/Widgets/Classroom/MyAsGraderClassroomList/MyAsGraderClassroomList.vue

@@ -0,0 +1,235 @@
+<template>
+  <div class="MyAsGraderClassroomList">
+    <entity-index v-if="mounted"
+                  ref="entityIndex"
+                  v-model:value="inputs"
+                  :title="'لیست ' + classroomTypeTitle"
+                  :api="api"
+                  :table="table"
+                  :table-keys="tableKeys"
+                  :show-expand-button="false"
+                  :show-reload-button="false"
+                  :show-search-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 === 'action'">
+          <div class="action-column-entity-index">
+            <q-btn size="md"
+                   color="primary"
+                   label="جزییات"
+                   :to="{name: 'UserPanel.Classroom.MyAsGraderClassroom.AnswerBooks', params: {classroom_id: inputData.props.row.id}}"
+                   class="q-mr-md" />
+          </div>
+        </template>
+        <template v-else>
+          {{ inputData.col.value }}
+        </template>
+      </template>
+    </entity-index>
+  </div>
+</template>
+
+<script>
+import { EntityIndex } from 'quasar-crud'
+import Enums from 'src/assets/Enums/Enums.js'
+import ShamsiDate from 'src/assets/ShamsiDate.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { UnitCategory } from 'src/models/UnitCategory.js'
+
+export default {
+  name: 'MyAsGraderClassroomList',
+  components: {
+    EntityIndex
+  },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      mounted: false,
+      inputs: [],
+      defaultOptions: {
+        classroomType: 'TRAINING'
+      },
+      api: APIGateway.classroom.APIAdresses.myAsGraderClassrooms,
+      table: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'id',
+            required: true,
+            label: 'شناسه',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'عنوان',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'audience_gender_type',
+            required: true,
+            label: 'جنسیت',
+            align: 'left',
+            field: row => (row.audience_gender_type === 'FEMALE') ? 'خواهران' : (row.audience_gender_type === 'MALE') ? 'برادران' : 'خواهران و برادران'
+          },
+          {
+            name: 'status',
+            required: true,
+            label: 'وضعیت',
+            align: 'left',
+            field: row => this.getClassroomStatusesTitle(row.status)
+          },
+          {
+            name: 'holding_type',
+            required: true,
+            label: 'نوع برگزاری',
+            align: 'left',
+            field: row => this.getClassroomHoldingTypeTitle(row.holding_type)
+          },
+          {
+            name: 'price',
+            required: true,
+            label: 'هزینه(ریال)',
+            align: 'left',
+            field: row => row.price
+          },
+          {
+            name: 'professor_info',
+            required: true,
+            label: 'استاد مربوطه',
+            align: 'left',
+            field: row => row.professor_info?.firstname + ' ' + row.professor_info?.lastname
+          },
+          {
+            name: 'beginning_registration_period',
+            required: true,
+            label: 'تاریخ و زمان ثبت نام',
+            align: 'left',
+            field: row => row.beginning_registration_period ? ShamsiDate.getDateTime(row.beginning_registration_period) : '-'
+          },
+          {
+            name: 'beginning_registration_period',
+            required: true,
+            label: 'ترم برگزاری',
+            align: 'left',
+            field: row => (row.beginning_registration_period) ? ShamsiDate.getTerm(row.beginning_registration_period) : '-'
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'عملیات',
+            align: 'left',
+            field: ''
+          }
+        ],
+        data: []
+      },
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      }
+    }
+  },
+  computed: {
+    classroomTypeTitle () {
+      const unitCategory = new UnitCategory({ type: this.localOptions.classroomType })
+      return unitCategory.type_info.label
+    },
+    createPageRouteName () {
+      if (this.localOptions.classroomType === 'TRAINING') {
+        return 'Admin.Classroom.Create'
+      }
+      if (this.localOptions.classroomType === 'EVENT') {
+        return 'Admin.Event.Create'
+      }
+      return 'Admin.Classroom.Create'
+    },
+    showPageRouteName () {
+      if (this.localOptions.classroomType === 'TRAINING') {
+        return 'Admin.Classroom.Show'
+      }
+      if (this.localOptions.classroomType === 'EVENT') {
+        return 'Admin.Event.Show'
+      }
+      return 'Admin.Classroom.Show'
+    }
+  },
+  mounted () {
+    this.setClassroomTypeOfInputs()
+    this.mounted = true
+  },
+  methods: {
+    setClassroomTypeOfInputs () {
+      this.table.columns.forEach(col => {
+        if (col.name === 'title') {
+          col.label = 'نام ' + this.classroomTypeTitle
+        }
+        if (col.name === 'professor_info') {
+          col.label = (this.localOptions.classroomType === 'TRAINING') ? 'استاد مربوطه' : 'برگزار کننده'
+        }
+      })
+    },
+    search () {
+      this.$refs.entityIndex.search()
+    },
+    getSelectOptions (result, value, label) {
+      return result.map(item => {
+        return {
+          value: item[value],
+          label: item[label]
+        }
+      })
+    },
+    getRemoveMessage (row) {
+      return 'آیا از حذف ' + row.title + ' اطمینان دارید؟'
+    },
+    getClassroomHoldingTypeTitle (type) {
+      const target = Enums.classroomHoldingTypes.find(item => item.value === type)
+      if (!target) {
+        return '-'
+      }
+
+      return target.label
+    },
+    getClassroomStatusesTitle (type) {
+      const target = Enums.classroomStatuses.find(item => item.value === type)
+      if (!target) {
+        return '-'
+      }
+
+      return target.label
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.MyAsGraderClassroomList {
+  .add-new-action-row {
+    display: flex;
+    flex-flow: row;
+    align-items: center;
+    justify-content: flex-end;
+    margin-bottom: 22px;
+  }
+  .action-column-entity-index {
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+  }
+}
+</style>

+ 28 - 0
src/components/Widgets/Classroom/MyAsGraderClassroomList/OptionPanel.vue

@@ -0,0 +1,28 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      <q-option-group v-model="localOptions.classroomType"
+                      :options="classroomTypeOptions" />
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { UnitCategory } from 'src/models/UnitCategory.js'
+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],
+  data () {
+    return {
+      classroomTypeOptions: (new UnitCategory()).typeEnums
+    }
+  }
+})
+</script>

+ 288 - 0
src/components/Widgets/Test/AdminSendTestGrade/AdminSendTestGrade.vue

@@ -0,0 +1,288 @@
+<template>
+  <div class="TestShow"
+       :style="localOptions.style">
+    <q-card v-if="!answerBook.loading && answerBook.id">
+      <q-card-section class="flex justify-between">
+        <div>{{ answerBook.test_info.title }}</div>
+      </q-card-section>
+      <q-separator />
+      <q-banner>
+        <div class="row">
+          <div class="col-md-4 col-12">
+            {{ toShamsi(answerBook.attending_start_time) }}
+          </div>
+          <div class="col-md-4 col-12">
+            {{ answerBook.test_info.test_set_info.test_set_questions.length }}
+            سوال
+          </div>
+          <div class="col-md-4 col-12">
+            {{ answerBook.duration }}
+            دقیقه
+          </div>
+        </div>
+      </q-banner>
+      <q-list separator>
+        <q-item v-for="(answerSheet, answerSheetIndex) in answerBook.answer_sheet_info.list"
+                :key="answerSheetIndex"
+                clickable>
+          <q-item-section>
+            <q-item-label v-html="'<span>' + (answerSheetIndex + 1) + ' - </span>' + answerSheet.test_set_question_info.question_info.text" />
+            <q-item-label>
+              <entity-show v-if="mounted"
+                           ref="entityShow"
+                           v-model:value="questionInputs[answerSheetIndex]"
+                           :loaded-data="answerSheet"
+                           :show-close-button="false"
+                           :show-edit-button="false"
+                           :show-expand-button="false"
+                           :show-save-button="false"
+                           :show-index-button="false"
+                           :show-reload-button="false"
+                           :default-layout="false" />
+            </q-item-label>
+          </q-item-section>
+
+          <q-item-section side>
+            <q-badge v-if="answerSheet.score !== null">
+              {{ answerSheet.score }}
+              از
+              {{ answerSheet.test_set_question_info.question_info.mark }}
+              نمره
+            </q-badge>
+            <q-badge v-else>
+              {{ answerSheet.test_set_question_info.question_info.mark }}
+              نمره
+            </q-badge>
+          </q-item-section>
+        </q-item>
+      </q-list>
+      <q-separator class="q-my-lg" />
+      <div class="q-pa-lg">
+        <entity-show v-if="mounted"
+                     ref="entityShow"
+                     v-model:value="overallAnswerInput"
+                     :loaded-data="answerBook"
+                     :show-close-button="false"
+                     :show-edit-button="false"
+                     :show-expand-button="false"
+                     :show-save-button="false"
+                     :show-index-button="false"
+                     :show-reload-button="false"
+                     :default-layout="false" />
+      </div>
+      <template v-if="$route.query.send_objection && $route.query.send_objection.toString() === '1'">
+        <q-separator class="q-my-xl" />
+        <q-card-section>
+          <entity-edit v-if="mounted"
+                       ref="objectionEntityEdit"
+                       v-model:value="objectionInputs"
+                       title="ثبت اعتراض"
+                       :api="objectionApi"
+                       :loaded-data="{}"
+                       :show-close-button="false"
+                       :show-edit-button="false"
+                       :show-expand-button="false"
+                       :show-save-button="false"
+                       :show-reload-button="false">
+            <template #after-form-builder>
+              <div class="flex justify-end q-mt-md">
+                <q-btn color="primary"
+                       label="ثبت اعتراض"
+                       @click="sendObjection" />
+              </div>
+            </template>
+          </entity-edit>
+        </q-card-section>
+      </template>
+      <template v-else-if="answerBook.objection_request">
+        <div>
+          متن اعتراض:
+        </div>
+        <div v-html="answerBook.objection_request" />
+      </template>
+      <template v-if="answerBook.objection_result">
+        <div>
+          نتیجه اعتراض:
+        </div>
+        <div v-html="answerBook.objection_result" />
+      </template>
+    </q-card>
+    <div v-else>
+      کمی صبر کنید...
+    </div>
+  </div>
+</template>
+
+<script>
+import Assist from 'src/assets/js/Assist.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { EntityShow, EntityEdit } from 'quasar-crud'
+import { AnswerBook } from 'src/models/AnswerBook.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+
+export default {
+  name: 'TestShow',
+  components: { EntityShow, EntityEdit },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      mounted: false,
+      overallAnswerInput: [
+        { type: 'input', name: 'overall_answer_text', responseKey: 'overall_answer_text', label: 'متن پاسخ جامع', placeholder: ' ', inputType: 'textarea', col: 'col-12' },
+        { type: 'file', name: 'overall_answer_attachment', responseKey: 'overall_answer_attachment', label: 'فایل پیوست جامع', placeholder: ' ', col: 'col-12' }
+      ],
+      questionInput: [
+        { type: 'input', name: 'answer_text', responseKey: 'answer_text', label: 'متن پاسخ', placeholder: ' ', inputType: 'textarea', col: 'col-12' },
+        { type: 'file', name: 'answer_attachment', responseKey: 'answer_attachment', label: 'فایل پیوست', placeholder: ' ', col: 'col-12' }
+      ],
+      questionInputs: [],
+      objectionInputs: [
+        { type: 'input', name: 'objection_request', responseKey: 'objection_request', label: 'متن اعتراض', placeholder: ' ', inputType: 'textarea', col: 'col-12' }
+      ],
+      objectionApi: APIGateway.answerBook.APIAdresses.submitObjectionRequest(this.$route.params.answer_book_id),
+      participateType: null,
+      participateTypeDialog: false,
+      answerBook: new AnswerBook(),
+      remainingTime: 0,
+      timerInterval: null
+    }
+  },
+  computed: {
+    formattedRemainingTime () {
+      const hours = Math.floor(this.remainingTime / 3600)
+      const minutes = Math.floor((this.remainingTime % 3600) / 60)
+      const seconds = this.remainingTime % 60
+
+      // Add zero padding for hours, minutes, and seconds
+      const formattedHours = hours.toString().padStart(2, '0')
+      const formattedMinutes = minutes.toString().padStart(2, '0')
+      const formattedSeconds = seconds.toString().padStart(2, '0')
+
+      return formattedHours + ':' + formattedMinutes + ':' + formattedSeconds
+    }
+  },
+  mounted () {
+    // this.getAnswerBook()
+    this.getTestQuestions()
+    this.mounted = true
+  },
+  methods: {
+    sendObjection () {
+      this.answerBook.loading = true
+      this.$refs.objectionEntityEdit.editEntity(false)
+        .then(() => {
+          this.getTestQuestions()
+        })
+        .catch(() => {
+          this.getTestQuestions()
+        })
+    },
+    onAllQuestionsSending () {
+      this.answerBook.loading = true
+    },
+    onAllQuestionsSentSuccess () {
+      this.answerBook.loading = false
+      this.backToClassList()
+    },
+    onAllQuestionsSentFailed () {
+      this.answerBook.loading = false
+    },
+    onSingleQuestionsSending () {
+      this.answerBook.loading = true
+    },
+    onSingleQuestionsSentSuccess () {
+      this.answerBook.loading = false
+      this.backToClassList()
+    },
+    onSingleQuestionsSentFailed () {
+      this.answerBook.loading = false
+    },
+    toShamsi (miladi) {
+      return Assist.miladiToShamsi(miladi)
+    },
+
+    startTest () {
+      this.$router.push({ name: this.participateType, params: { question_id: 1 } })
+    },
+    getRemainingTimeInSeconds (serverTimeStr, attendingStartTimeStr, durationInMinutes) {
+      // Convert the strings to Date objects
+      const serverTime = new Date(serverTimeStr)
+      const attendingStartTime = new Date(attendingStartTimeStr)
+
+      // Calculate the difference in milliseconds
+      const timeDiffInMilliseconds = serverTime - attendingStartTime
+
+      // Convert the difference to seconds
+      const timeDiffInSeconds = timeDiffInMilliseconds / 1000
+
+      // Calculate the remaining time in seconds considering the duration
+      return (durationInMinutes * 60) - timeDiffInSeconds
+    },
+    backToClassList () {
+      // this.$router.push({ name: 'UserPanel.Profile.AllClassrooms' })
+    },
+    getAnswerBook () {
+      this.answerBook.loading = true
+      APIGateway.answerBook.get(this.$route.params.answer_book_id)
+        .then((answerBook) => {
+          this.answerBook = new AnswerBook(answerBook)
+          this.answerBook.loading = false
+        })
+        .catch(() => {
+          this.answerBook.loading = false
+          this.backToClassList()
+        })
+    },
+    getTestQuestions () {
+      this.answerBook.loading = true
+      APIGateway.answerBook.getTestQuestions(this.$route.params.answer_book_id)
+        .then((answerBook) => {
+          this.answerBook = new AnswerBook(answerBook)
+          this.answerBook.loading = false
+          this.answerBook.answer_sheet_info.list.forEach(() => {
+            this.questionInputs.push(this.questionInput)
+          })
+          this.remainingTime = this.getRemainingTimeInSeconds(this.answerBook.server_time, this.answerBook.attending_start_time, this.answerBook.duration)
+          FormBuilderAssist.setAttributeByName(this.objectionInputs, 'objection_request', 'value', this.answerBook.objection_request)
+        })
+        .catch(() => {
+          this.answerBook.loading = false
+          this.backToClassList()
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.TestShow {
+  :deep(.q-banner) {
+    background: #FCF9F4 0 0 no-repeat padding-box;
+    padding: 20px;
+    .q-banner__content {
+      font-size: 16px;
+      font-weight: normal;
+    }
+  }
+  :deep(.q-list) {
+    .q-item {
+      .q-item__section {
+        display: inline-block;
+        .entity-crud-formBuilder {
+          margin-top: 8px;
+          .outsideLabel {
+            margin-bottom: 8px;
+          }
+        }
+      }
+      .q-badge {
+        background: #ACBCAE 0 0 no-repeat padding-box;
+        border-radius: 18px;
+        height: 32px;
+        padding: 4px 16px;
+      }
+    }
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Test/AdminSendTestGrade/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>

+ 267 - 0
src/components/Widgets/Test/AdminShowTestGrade/AdminShowTestGrade.vue

@@ -0,0 +1,267 @@
+<template>
+  <div class="AdminShowTestGrade"
+       :style="localOptions.style">
+    <q-card v-if="!answerBook.loading && answerBook.id">
+      <q-card-section class="flex justify-between">
+        <div>{{ answerBook.test_info.title }}</div>
+      </q-card-section>
+      <q-separator />
+      <q-banner>
+        وضعیت آزمون:
+        {{ answerBook.status_info.label }}
+      </q-banner>
+      <q-btn v-if="!this.answerBook.grader"
+             color="primary"
+             class="full-width"
+             @click="setGraderToMe">
+        تصحیح توسط شما
+      </q-btn>
+      <q-separator />
+      <q-banner>
+        <div class="row">
+          <div class="col-md-4 col-12">
+            {{ toShamsi(answerBook.attending_start_time) }}
+          </div>
+          <div class="col-md-4 col-12">
+            {{ answerBook.test_info.test_set_info.test_set_questions.length }}
+            سوال
+          </div>
+          <div class="col-md-4 col-12">
+            {{ answerBook.duration }}
+            دقیقه
+          </div>
+        </div>
+      </q-banner>
+      <q-list separator>
+        <q-item v-for="(answerSheet, answerSheetIndex) in answerBook.answer_sheet_info.list"
+                :key="answerSheetIndex"
+                clickable>
+          <q-item-section>
+            <q-item-label v-html="'<span>' + (answerSheetIndex + 1) + ' - </span>' + answerSheet.test_set_question_info.question_info.text" />
+            <q-item-label>
+              <entity-show v-if="mounted"
+                           ref="entityShow"
+                           v-model:value="questionInputs[answerSheetIndex]"
+                           :loaded-data="answerSheet"
+                           :show-close-button="false"
+                           :show-edit-button="false"
+                           :show-expand-button="false"
+                           :show-save-button="false"
+                           :show-index-button="false"
+                           :show-reload-button="false"
+                           :default-layout="false" />
+            </q-item-label>
+            <q-item-label v-if="currentUserIsGrader">
+              <q-input v-model="questionInputs[answerSheetIndex].score"
+                       :loading="questionInputs[answerSheetIndex].loading"
+                       label="نمره">
+                <template v-slot:append>
+                  <q-btn color="primary"
+                         flat
+                         label="ثبت نمره"
+                         :loading="questionInputs[answerSheetIndex].loading"
+                         @click="submitScore(answerSheetIndex)" />
+                </template>
+              </q-input>
+            </q-item-label>
+          </q-item-section>
+
+          <q-item-section side>
+            <q-badge v-if="answerSheet.score !== null">
+              {{ answerSheet.score }}
+              از
+              {{ answerSheet.test_set_question_info.question_info.mark }}
+              نمره
+            </q-badge>
+            <q-badge v-else>
+              {{ answerSheet.test_set_question_info.question_info.mark }}
+              نمره
+            </q-badge>
+          </q-item-section>
+        </q-item>
+      </q-list>
+      <q-separator class="q-my-lg" />
+      <div class="q-pa-lg">
+        <entity-show v-if="mounted"
+                     ref="entityShow"
+                     v-model:value="overallAnswerInput"
+                     :loaded-data="answerBook"
+                     :show-close-button="false"
+                     :show-edit-button="false"
+                     :show-expand-button="false"
+                     :show-save-button="false"
+                     :show-index-button="false"
+                     :show-reload-button="false"
+                     :default-layout="false" />
+      </div>
+      <template v-if="$route.query.send_objection && $route.query.send_objection.toString() === '1'">
+        <q-separator class="q-my-xl" />
+        <q-card-section>
+          <entity-edit v-if="mounted"
+                       ref="objectionEntityEdit"
+                       v-model:value="objectionInputs"
+                       title="ثبت اعتراض"
+                       :api="objectionApi"
+                       :loaded-data="{}"
+                       :show-close-button="false"
+                       :show-edit-button="false"
+                       :show-expand-button="false"
+                       :show-save-button="false"
+                       :show-reload-button="false">
+            <template #after-form-builder>
+              <div class="flex justify-end q-mt-md">
+                <q-btn color="primary"
+                       label="ثبت اعتراض"
+                       @click="sendObjection" />
+              </div>
+            </template>
+          </entity-edit>
+        </q-card-section>
+      </template>
+      <template v-else-if="answerBook.objection_request">
+        <div>
+          متن اعتراض:
+        </div>
+        <div v-html="answerBook.objection_request" />
+      </template>
+      <template v-if="answerBook.objection_result">
+        <div>
+          نتیجه اعتراض:
+        </div>
+        <div v-html="answerBook.objection_result" />
+      </template>
+    </q-card>
+    <div v-else>
+      کمی صبر کنید...
+    </div>
+  </div>
+</template>
+
+<script>
+import Assist from 'src/assets/js/Assist.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { EntityShow, EntityEdit } from 'quasar-crud'
+import { AnswerBook } from 'src/models/AnswerBook.js'
+import { mixinWidget, mixinAuth } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'AdminShowTestGrade',
+  components: { EntityShow, EntityEdit },
+  mixins: [mixinWidget, mixinAuth],
+  data () {
+    return {
+      mounted: false,
+      currentUserIsGrader: false,
+      overallAnswerInput: [
+        { type: 'input', name: 'overall_answer_text', responseKey: 'overall_answer_text', label: 'متن پاسخ جامع', placeholder: ' ', inputType: 'textarea', col: 'col-12' },
+        { type: 'file', name: 'overall_answer_attachment', responseKey: 'overall_answer_attachment', label: 'فایل پیوست جامع', placeholder: ' ', col: 'col-12' }
+      ],
+      questionInput: [
+        { type: 'input', name: 'answer_text', responseKey: 'answer_text', label: 'متن پاسخ', placeholder: ' ', inputType: 'textarea', col: 'col-12' },
+        { type: 'file', name: 'answer_attachment', responseKey: 'answer_attachment', label: 'فایل پیوست', placeholder: ' ', col: 'col-12' }
+      ],
+      questionInputs: [],
+      objectionInputs: [
+        { type: 'input', name: 'objection_request', responseKey: 'objection_request', label: 'متن اعتراض', placeholder: ' ', inputType: 'textarea', col: 'col-12' }
+      ],
+      objectionApi: APIGateway.answerBook.APIAdresses.submitObjectionRequest(this.$route.params.answer_book_id),
+      participateType: null,
+      participateTypeDialog: false,
+      answerBook: new AnswerBook(),
+      timerInterval: null
+    }
+  },
+  mounted () {
+    this.getAnswerBook()
+    this.mounted = true
+  },
+  methods: {
+    sendObjection () {
+      this.answerBook.loading = true
+      this.$refs.objectionEntityEdit.editEntity(false)
+        .then(() => {
+          this.getTestQuestions()
+        })
+        .catch(() => {
+          this.getTestQuestions()
+        })
+    },
+    toShamsi (miladi) {
+      return Assist.miladiToShamsi(miladi)
+    },
+    loadAnswerBook (answerBook) {
+      this.answerBook = new AnswerBook(answerBook)
+      this.answerBook.answer_sheet_info.list.forEach(() => {
+        this.questionInputs.push(this.questionInput)
+      })
+      this.currentUserIsGrader = this.answerBook.grader === this.user.id
+    },
+    setGraderToMe () {
+      this.answerBook.loading = true
+      APIGateway.answerBook.setGraderToMe(this.$route.params.answer_book_id)
+        .then((answerBook) => {
+          this.loadAnswerBook(answerBook)
+          this.answerBook.loading = false
+        })
+        .catch(() => {
+          this.getAnswerBook()
+          this.answerBook.loading = false
+        })
+    },
+    submitScore (index) {
+      this.questionInputs[index].loading = true
+      APIGateway.answerSheet.submitScore(this.$route.params.answer_book_id, this.questionInputs[index])
+        .then(() => {
+          this.questionInputs[index].loading = false
+          this.getAnswerBook()
+        })
+        .catch(() => {
+          this.questionInputs[index].loading = false
+        })
+    },
+    getAnswerBook () {
+      this.answerBook.loading = true
+      APIGateway.answerBook.get(this.$route.params.answer_book_id)
+        .then((answerBook) => {
+          this.loadAnswerBook(answerBook)
+          this.answerBook.loading = false
+        })
+        .catch(() => {
+          this.answerBook.loading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminShowTestGrade {
+  :deep(.q-banner) {
+    background: #FCF9F4 0 0 no-repeat padding-box;
+    padding: 20px;
+    .q-banner__content {
+      font-size: 16px;
+      font-weight: normal;
+    }
+  }
+  :deep(.q-list) {
+    .q-item {
+      .q-item__section {
+        display: inline-block;
+        .entity-crud-formBuilder {
+          margin-top: 8px;
+          .outsideLabel {
+            margin-bottom: 8px;
+          }
+        }
+      }
+      .q-badge {
+        background: #ACBCAE 0 0 no-repeat padding-box;
+        border-radius: 18px;
+        height: 32px;
+        padding: 4px 16px;
+      }
+    }
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Test/AdminShowTestGrade/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>

+ 4 - 0
src/models/AnswerBook.js

@@ -68,6 +68,10 @@ class AnswerBook extends Model {
         infoKey: 'status_info',
         enumListKey: 'statusEnums',
         enums: [
+          {
+            label: 'جاری',
+            value: 'ONGOING'
+          },
           {
             label: 'حذف شده',
             value: 'DELETED'

+ 23 - 0
src/pages/Admin/Classroom/ClassroomAnswerBookShowGrade.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: 'UserPanel.Classroom.MyAsGraderClassroom.AnswerBooks.ShowGrade',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminShowTestGrade' }] }] }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 23 - 0
src/pages/Admin/Classroom/ClassroomAnswerBookSubmitGrade.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: 'UserPanel.Classroom.MyAsGraderClassroom.AnswerBooks.SubmitGrade',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminSendTestGrade' }] }] }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

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

+ 23 - 0
src/pages/Admin/Classroom/MyAsGraderClassrooms.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: 'MyAsGraderClassroomList' }] }] }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 12 - 1
src/router/routes.js

@@ -336,6 +336,17 @@ const routes = [
                   { name: 'UserPanel.Test.AnswerBook.Show', path: 'answer_book/:answer_book_id/show', component: () => import('src/pages/User/Test/Show.vue') },
                   { name: 'UserPanel.Test.AnswerBook.SendObjection', path: 'answer_book/:answer_book_id/sendObjection', component: () => import('src/pages/User/Test/SendObjection.vue') }
                 ]
+              },
+              {
+                name: 'UserPanel.Classroom.MyAsGraderClassroom',
+                path: 'my_as_a_grader_classrooms',
+                component: () => import('src/layouts/bareLayout.vue'),
+                children: [
+                  { name: 'UserPanel.Classroom.MyAsGraderClassroom.Index', path: '', component: () => import('src/pages/Admin/Classroom/MyAsGraderClassrooms.vue') },
+                  { name: 'UserPanel.Classroom.MyAsGraderClassroom.AnswerBooks', path: ':classroom_id/answer_books', component: () => import('src/pages/Admin/Classroom/ClassroomAnswerBooks.vue') },
+                  { name: 'UserPanel.Classroom.MyAsGraderClassroom.AnswerBooks.ShowGrade', path: ':classroom_id/answer_books/:answer_book_id/show_grades', component: () => import('src/pages/Admin/Classroom/ClassroomAnswerBookShowGrade.vue') },
+                  { name: 'UserPanel.Classroom.MyAsGraderClassroom.AnswerBooks.SubmitGrade', path: ':classroom_id/answer_books/:answer_book_id/submit_grade', component: () => import('src/pages/Admin/Classroom/ClassroomAnswerBookSubmitGrade.vue') }
+                ]
               }
             ]
           },
@@ -658,7 +669,7 @@ const routes = [
               { 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.TestSet',
+                name: 'Admin.Classroom.Group',
                 path: ':classroom_id',
                 component: () => import('src/layouts/bareLayout.vue'),
                 children: [

+ 9 - 9
yarn.lock

@@ -5642,23 +5642,23 @@ quasar-app-extension-q-page-builder@^0.1.71:
   dependencies:
     quasar-ui-q-page-builder "^0.1.71"
 
-quasar-crud@^0.3.55:
-  version "0.3.55"
-  resolved "https://registry.yarnpkg.com/quasar-crud/-/quasar-crud-0.3.55.tgz#b6fe74eb8538fe671344622a1fa1cf62d942c8a2"
-  integrity sha512-Lr5HR1tgYeT7VhM2UXlAj7SItgsYy0tCwO8ptLlV8h8cij7329PISjzEwa2CIYyf8AbsWRDZc3wgTkOMP+062Q==
+quasar-crud@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/quasar-crud/-/quasar-crud-0.4.0.tgz#d13b2403aadb171c37656990dab98e57384cfc59"
+  integrity sha512-76lzdWRRZGF5mKWd5+6ZSSAKVJM4kReUJephH0vtRoazoYd3jwaevbuEwNleXjs+MPxm8zysyDiQOlrPrYI65Q==
   dependencies:
     "@quasar/extras" "^1.16.5"
     axios "^1.4.0"
     moment-jalaali "^0.10.0"
     quasar "^2.12.2"
-    quasar-form-builder "^0.1.110"
+    quasar-form-builder "^0.2.0"
     shvl "^3.0.0"
     vue "^3.2.47"
 
-quasar-form-builder@^0.1.110:
-  version "0.1.110"
-  resolved "https://registry.yarnpkg.com/quasar-form-builder/-/quasar-form-builder-0.1.110.tgz#cdc1baf57bf9567387803d44ea4ff5a1acdda024"
-  integrity sha512-7Q8FZFgXu7FlV9Gqf06+efLEl3OptHcG6cC6Sps97GJ8/q3/1NcTFu8PEoQKQkBWDA4ZSaz+9fv2aRJLvWM8lA==
+quasar-form-builder@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/quasar-form-builder/-/quasar-form-builder-0.2.0.tgz#3ca0711e17e7fe4cf93f2944ef02915b0af0b1eb"
+  integrity sha512-YpqySdrtkyQJyrr0Le+Thg8EbXsU66fj3MWj/AWSFG87l6lY8K7pbaQSVp9dBZyksX7SHppRUQhUWW05/thQ2A==
   dependencies:
     "@quasar/extras" "^1.16.5"
     core-js "^3.31.1"