Jelajahi Sumber

Merge branch 'master' of http://git.hghweb.ir/ali.s/palizvani-front-vite into production

hgh 1 tahun lalu
induk
melakukan
f02275ccd0
57 mengubah file dengan 2471 tambahan dan 229 penghapusan
  1. 2 2
      package.json
  2. 2 0
      src/api/APIGateway.js
  3. 3 2
      src/api/models/AnswerSheet.js
  4. 38 0
      src/api/models/Classroom.js
  5. 19 1
      src/api/models/ProjectAttendanceSheets.js
  6. 3 1
      src/api/models/Registration.js
  7. 2 1
      src/api/models/Test.js
  8. 85 0
      src/api/models/TranscriptSheetHistory.js
  9. 3 1
      src/api/models/User.js
  10. 83 19
      src/components/FormBuilderCustumComponents/ContentCategorySelector.vue
  11. 15 1
      src/components/FormBuilderCustumComponents/QuestionsCreateAndAppend/QuestionsCreateAndAppend.vue
  12. 12 1
      src/components/Widgets/Classroom/AdminClassroomAnswerBookList/AdminClassroomAnswerBookList.vue
  13. 74 8
      src/components/Widgets/Classroom/AdminClassroomList/AdminClassroomList.vue
  14. 2 1
      src/components/Widgets/Classroom/AdminClassroomShow/AdminClassroomShow.vue
  15. 272 3
      src/components/Widgets/Classroom/AdminClassroomShow/components/MembersList.vue
  16. 1 1
      src/components/Widgets/Classroom/AdminClassroomShow/components/SessionList.vue
  17. 5 1
      src/components/Widgets/Classroom/ClassroomAnswerBookList/ClassroomAnswerBookList.vue
  18. 1 1
      src/components/Widgets/Content/AdminContentCreate/AdminContentCreate.vue
  19. 1 1
      src/components/Widgets/Content/AdminContentShow/AdminContentShow.vue
  20. 2 6
      src/components/Widgets/Content/ContentShow/ContentShow.vue
  21. 1 1
      src/components/Widgets/Content/ContentShow/components/MediaCategory.vue
  22. 6 2
      src/components/Widgets/Content/ContentShow/components/MediaItem.vue
  23. 66 61
      src/components/Widgets/ContentCategory/ContentCategoryList/ContentCategoryList.vue
  24. 78 0
      src/components/Widgets/ContentCategory/ContentCategoryList/ContentItem.vue
  25. 102 0
      src/components/Widgets/ContentCategory/ContentCategoryList/ContentItemCategories.vue
  26. 200 0
      src/components/Widgets/ContentCategory/ContentCategoryList/filters.vue
  27. 55 56
      src/components/Widgets/ContentCategory/ContentCategoryShow/ContentCategoryShow.vue
  28. 2 1
      src/components/Widgets/Event/AdminEventShow/AdminEventShow.vue
  29. 41 17
      src/components/Widgets/ProjectAttendanceSheet/AdminProjectAttendanceSheetShow/AdminProjectAttendanceSheetShow.vue
  30. 2 1
      src/components/Widgets/SessionAttendanceSheet/AdminSessionAttendanceSheetList/AdminSessionAttendanceSheetList.vue
  31. 1 1
      src/components/Widgets/Store/Shop/components/productItem.vue
  32. 1 1
      src/components/Widgets/Store/StorePackage/StorePackageShow/StorePackageShow.vue
  33. 1 1
      src/components/Widgets/Store/StoreProduct/ProductCarousel/productItem.vue
  34. 1 1
      src/components/Widgets/Store/StoreProduct/StoreProductShow/StoreProductShow.vue
  35. 1 1
      src/components/Widgets/Store/StoreProduct/StoreRelatedProducts/components/productItem.vue
  36. 207 18
      src/components/Widgets/Test/AdminAnswerBookShow/AdminAnswerBookShow.vue
  37. 129 0
      src/components/Widgets/Test/ShowTestGradeForGrader/ShowTestGradeForGrader.vue
  38. 99 1
      src/components/Widgets/Test/TestShow/TestShow.vue
  39. 102 0
      src/components/Widgets/TranscriptSheetHistory/AdminTranscriptSheetHistoryCreate/AdminTranscriptSheetHistoryCreate.vue
  40. 21 0
      src/components/Widgets/TranscriptSheetHistory/AdminTranscriptSheetHistoryCreate/OptionPanel.vue
  41. 133 0
      src/components/Widgets/TranscriptSheetHistory/AdminTranscriptSheetHistoryIndex/AdminTranscriptSheetHistoryIndex.vue
  42. 21 0
      src/components/Widgets/TranscriptSheetHistory/AdminTranscriptSheetHistoryIndex/OptionPanel.vue
  43. 176 0
      src/components/Widgets/TranscriptSheetHistory/AdminTranscriptSheetHistoryShow/AdminTranscriptSheetHistoryShow.vue
  44. 21 0
      src/components/Widgets/TranscriptSheetHistory/AdminTranscriptSheetHistoryShow/OptionPanel.vue
  45. 10 0
      src/css/Theme/Controls/input.scss
  46. 25 2
      src/models/AnswerBook.js
  47. 4 0
      src/models/Media.js
  48. 2 1
      src/models/SessionAttendanceSheets.js
  49. 26 0
      src/models/TranscriptSheetHistory.js
  50. 23 0
      src/pages/Admin/TranscriptSheetHistory/Create.vue
  51. 23 0
      src/pages/Admin/TranscriptSheetHistory/Index.vue
  52. 23 0
      src/pages/Admin/TranscriptSheetHistory/Show.vue
  53. 140 0
      src/pages/Admin/Unit/show.vue
  54. 82 2
      src/pages/User/Profile/classroomInfo.vue
  55. 3 1
      src/pages/User/Profile/projectInfo.vue
  56. 9 0
      src/router/routes/Admin.js
  57. 9 9
      yarn.lock

+ 2 - 2
package.json

@@ -48,8 +48,8 @@
     "process": "^0.11.10",
     "quasar": "^2.14.2",
     "quasar-app-extension-q-page-builder": "^0.4.4",
-    "quasar-crud": "^2.2.5",
-    "quasar-form-builder": "^3.5.2",
+    "quasar-crud": "^2.2.7",
+    "quasar-form-builder": "^3.5.4",
     "quasar-template-builder": "^1.0.23",
     "quasar-ui-q-page-builder": "^0.4.4",
     "socket.io-client": "^4.6.1",

+ 2 - 0
src/api/APIGateway.js

@@ -45,6 +45,7 @@ import QuestionChoiceApi from 'src/api/models/QuestionChoice.js'
 import TicketCategoryAPI from 'src/api/models/TicketCategory.js'
 import SessionTemplateAPI from 'src/api/models/SessionTemplate.js'
 import TranscriptSheetAPI from 'src/api/models/TranscriptSheet.js'
+import TranscriptSheetHistoryAPI from 'src/api/models/TranscriptSheetHistory.js'
 import ContentCategoryAPI from 'src/api/models/ContentCategory.js'
 import InstalmentOfferAPI from 'src/api/models/InstalmentOffer.js'
 import ClassroomGraderAPI from 'src/api/models/ClassroomGraders.js'
@@ -105,6 +106,7 @@ export const APIGateway = {
   ticketCategory: new TicketCategoryAPI(),
   questionChoice: new QuestionChoiceApi(),
   transcriptSheet: new TranscriptSheetAPI(),
+  transcriptSheetHistory: new TranscriptSheetHistoryAPI(),
   sessionTemplate: new SessionTemplateAPI(),
   contentCategory: new ContentCategoryAPI(),
   instalmentOffer: new InstalmentOfferAPI(),

+ 3 - 2
src/api/models/AnswerSheet.js

@@ -9,7 +9,8 @@ export default class AnswerSheetAPI extends APIRepository {
       base: '/lma/answer_sheets',
       byId: (id) => '/lma/answer_sheets/' + id,
       getTestQuestions: (id) => '/lma/answer_sheets/' + id + '/get_test_questions',
-      submitOverallAnswer: (id) => '/lma/answer_sheets/' + id + '/submit_overall_answer'
+      submitOverallAnswer: (id) => '/lma/answer_sheets/' + id + '/submit_overall_answer',
+      submitScore: (id) => '/lma/answer_sheets/' + id + '/submit_score'
     }
     this.CacheList = {
       base: this.name + this.APIAdresses.base,
@@ -92,7 +93,7 @@ export default class AnswerSheetAPI extends APIRepository {
     return this.sendRequest({
       apiMethod: 'put',
       api: this.api,
-      request: this.APIAdresses.byId(answerSheetsId),
+      request: this.APIAdresses.submitScore(answerSheetsId),
       data: {
         score
       },

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

@@ -13,9 +13,11 @@ export default class ClassroomAPI extends APIRepository {
       byId: (id) => '/lma/classrooms/' + id,
       toggleOngoingStatus: (id) => '/lma/classrooms/' + id + '/toggle_ongoing_status',
       enroll: (id) => '/lma/classrooms/' + id + '/enroll',
+      bulkEnroll: (id) => '/lma/classrooms/' + id + '/bulk_enroll',
       graders: (id) => '/lma/classrooms/' + id + '/graders',
       drop: (id) => '/lma/classrooms/' + id + '/drop',
       register: (id) => '/lma/classrooms/' + id + '/register',
+      bulkRegister: (id) => '/lma/classrooms/' + id + '/bulk_register',
       leaders: (id) => '/lma/classrooms/' + id + '/leaders',
       announceResult: (id) => '/lma/classrooms/' + id + '/announce_result',
       getClassroomAndCorrespondingRegistration: (classroomId, ownerId) => '/lma/classrooms/' + classroomId + '/get_classroom_and_corresponding_registration/' + ownerId,
@@ -250,6 +252,42 @@ export default class ClassroomAPI extends APIRepository {
     })
   }
 
+  registerNewMembers (data) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.bulkRegister(data.classroomId),
+      data: {
+        classroomId: data.classroomId,
+        owners: data.owners
+      },
+      resolveCallback: (response) => {
+        return new ClassroomList(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  enrollNewMembers (data) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.bulkEnroll(data.classroomId),
+      data: {
+        classroomId: data.classroomId,
+        owners: data.owners
+      },
+      resolveCallback: (response) => {
+        return new ClassroomList(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
   createInvoiceByAdmin (classroomId, userId) {
     return this.sendRequest({
       apiMethod: 'put',

+ 19 - 1
src/api/models/ProjectAttendanceSheets.js

@@ -10,7 +10,8 @@ export default class ProjectAttendanceSheetsAPI extends APIRepository {
       project_sheets: '/lma/project_attendance_sheets/project_sheets',
       byId: (id) => '/lma/project_attendance_sheets/' + id,
       verify: (id) => '/lma/project_attendance_sheets/' + id + '/verify',
-      notVerify: (id) => '/lma/project_attendance_sheets/' + id + '/not_verify'
+      notVerify: (id) => '/lma/project_attendance_sheets/' + id + '/not_verify',
+      saveNotation: (id) => '/lma/project_attendance_sheets/' + id + '/save_notation'
     }
     this.CacheList = {
       base: this.name + this.APIAdresses.base,
@@ -111,4 +112,21 @@ export default class ProjectAttendanceSheetsAPI extends APIRepository {
       }
     })
   }
+
+  saveNotation (projectAttendanceSheetsId, notation) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.saveNotation(projectAttendanceSheetsId),
+      data: {
+        notation
+      },
+      resolveCallback: (response) => {
+        return new ProjectAttendanceSheets(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
 }

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

@@ -62,7 +62,9 @@ export default class RegistrationAPI extends APIRepository {
       data: this.getNormalizedSendData({
         classroom: null, // Number
         type: null, // String
-        status: null // String
+        status: null, // String
+        owner__national_code: null, // Number
+        invoice__status: null // Number
       }, data),
       responseType: 'blob',
       resolveCallback: (response) => {

+ 2 - 1
src/api/models/Test.js

@@ -97,7 +97,8 @@ export default class TestAPI extends APIRepository {
       request: this.APIAdresses.testQuestionById(data.id),
       data: {
         mark: data.mark,
-        is_extra_mark: data.is_extra_mark
+        is_extra_mark: data.is_extra_mark,
+        order: data.order
       },
       resolveCallback: () => {
         return null

+ 85 - 0
src/api/models/TranscriptSheetHistory.js

@@ -0,0 +1,85 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { TranscriptSheetHistory, TranscriptSheetHistoryList } from 'src/models/TranscriptSheetHistory.js'
+
+export default class TranscriptSheetHistoryAPI extends APIRepository {
+  constructor() {
+    super('TranscriptSheetHistory', appApi, '/lma/categories', TranscriptSheetHistory)
+    this.APIAdresses = {
+      base: '/lma/transcript_sheet_histories',
+      byId: (id) => '/lma/transcript_sheet_histories/' + id,
+      appendQuestion: (id) => '/lma/transcript_sheet_histories/' + id + '/append_question',
+      importList: '/lma/transcript_sheet_histories/import_list'
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base,
+      byId: (id) => this.name + this.APIAdresses.byId(id)
+    }
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        unit: null, // Number
+        category: null, // Number
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new TranscriptSheetHistoryList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  get (id) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.byId(id),
+      resolveCallback: (response) => {
+        return new TranscriptSheetHistory(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  create (data) {
+    return this.sendRequest({
+      apiMethod: 'post',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        title: null, // String
+        unit: null // Number
+      }, data),
+      resolveCallback: (response) => {
+        return new TranscriptSheetHistory(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 3 - 1
src/api/models/User.js

@@ -14,7 +14,9 @@ export default class UserAPI extends APIRepository {
       byId: (id) => '/uma/users/' + id,
       changeRole: (id) => '/uma/users/' + id + '/change_role',
       unassignedGradersWithLastPassedUnit: '/uma/users/unassigned_graders_with_last_passed_unit',
-      unassignedLeadersWithLastPassedUnit: '/uma/users/unassigned_leaders_with_last_passed_unit'
+      unassignedLeadersWithLastPassedUnit: '/uma/users/unassigned_leaders_with_last_passed_unit',
+      unenrolledUsersInClassroom: '/uma/users/unenrolled_users_in_classroom',
+      unregisteredUsersInClassroom: '/uma/users/unregistered_users_in_classroom'
     }
     this.CacheList = {
       base: this.name + this.APIAdresses.base,

+ 83 - 19
src/components/FormBuilderCustumComponents/ContentCategorySelector.vue

@@ -102,6 +102,11 @@ export default {
     mainCategory (newValue) {
       this.subCategory = null
       this.subCategoryOptions = []
+      if (this.categoryType === 'content') {
+        const selectedCategoryId = newValue?.value ? newValue?.value : null
+        this.$emit('update:value', selectedCategoryId)
+      }
+
       if (!newValue.item.children) {
         this.$emit('update:value', newValue.value)
         return
@@ -117,6 +122,17 @@ export default {
     subCategory (newValue) {
       this.bakhshCategory = null
       this.bakhshCategoryOptions = []
+      if (this.categoryType === 'content') {
+        let selectedCategoryId = newValue?.value ? newValue?.value : null
+        if (this.mainCategory?.value) {
+          selectedCategoryId = this.mainCategory.value
+        }
+        if (newValue?.value) {
+          selectedCategoryId = this.subCategory?.value
+        }
+        this.$emit('update:value', selectedCategoryId)
+      }
+
       if (!newValue?.item?.children) {
         this.$emit('update:value', null)
         return
@@ -130,25 +146,55 @@ export default {
       })
     },
     bakhshCategory (newValue) {
+      let selectedCategoryId = null
       const categoryId = newValue ? newValue.value : null
-      this.$emit('update:value', categoryId)
+      if (this.categoryType === 'content') {
+        if (this.mainCategory?.value) {
+          selectedCategoryId = this.mainCategory.value
+        }
+        if (this.subCategory?.value) {
+          selectedCategoryId = this.subCategory.value
+        }
+        if (categoryId) {
+          selectedCategoryId = categoryId
+        }
+      } else {
+        selectedCategoryId = categoryId
+      }
+      this.$emit('update:value', selectedCategoryId)
     }
   },
   mounted() {
     this.getCategories()
   },
   methods: {
-    setMainCategory (category) {
-      if (!category?.parent?.parent?.id) {
+    setMainCategory (category, categoryType = 'bakhshCategory') {
+      if (categoryType === 'bakhshCategory' && !category?.parent?.parent?.id) {
+        return
+      }
+      if (categoryType === 'subCategory' && !category?.parent?.id) {
+        return
+      }
+      if (categoryType === 'mainCategory' && !category?.id) {
         return
       }
-      this.mainCategory = this.mainCategoryOptions.find(item => item.value === category.parent.parent.id)
+      this.mainCategory = this.mainCategoryOptions.find(item => (
+        (categoryType === 'bakhshCategory' && item.value === category.parent.parent.id) ||
+        (categoryType === 'subCategory' && item.value === category.parent.id) ||
+        (categoryType === 'mainCategory' && item.value === category.id)
+      ))
     },
-    setSubCategory (category) {
-      if (!category?.parent?.id) {
+    setSubCategory (category, categoryType = 'bakhshCategory') {
+      if (categoryType === 'bakhshCategory' && !category?.parent?.id) {
         return
       }
-      this.subCategory = this.subCategoryOptions.find(item => item.value === category.parent.id)
+      if (categoryType === 'subCategory' && !category?.id) {
+        return
+      }
+      this.subCategory = this.subCategoryOptions.find(item => (
+        (categoryType === 'bakhshCategory' && item.value === category.parent.id) ||
+        (categoryType === 'subCategory' && item.value === category.id)
+      ))
     },
     setBakhshCategory (category) {
       if (!category?.id) {
@@ -156,6 +202,27 @@ export default {
       }
       this.bakhshCategory = this.bakhshCategoryOptions.find(item => item.value === category.id)
     },
+    setMainAndSubAndBakshCategory (category) {
+      this.$nextTick(() => {
+        this.setMainCategory(category)
+        this.$nextTick(() => {
+          this.setSubCategory(category)
+          this.$nextTick(() => {
+            this.setBakhshCategory(category)
+            this.$emit('update:value', category.id)
+          })
+        })
+      })
+    },
+    setMainAndSubCategory (category) {
+      this.$nextTick(() => {
+        this.setMainCategory(category, 'subCategory')
+        this.$nextTick(() => {
+          this.setSubCategory(category, 'subCategory')
+          this.$emit('update:value', category.id)
+        })
+      })
+    },
     getCategories () {
       this.categories.laoding = true
       const args = { parent__isnull: 'true', per_page: 99999 }
@@ -188,17 +255,15 @@ export default {
           })
 
           if (this.value?.parent?.parent?.id && this.value?.id) {
-            const category = this.value
-            this.$nextTick(() => {
-              this.setMainCategory(category)
-              this.$nextTick(() => {
-                this.setSubCategory(category)
-                this.$nextTick(() => {
-                  this.setBakhshCategory(category)
-                  this.$emit('update:value', category.id)
-                })
-              })
-            })
+            this.setMainAndSubAndBakshCategory(this.value)
+          }
+
+          if (this.categoryType === 'content') {
+            if (!this.value?.parent?.parent?.id && this.value?.parent?.id && this.value?.id) {
+              this.setMainAndSubCategory(this.value)
+            } else if (!this.value?.parent?.parent?.id && !this.value?.parent?.id && this.value?.id) {
+              this.setMainCategory(this.value, 'mainCategory')
+            }
           }
         })
         .catch(() => {
@@ -206,7 +271,6 @@ export default {
         })
     }
   }
-
 }
 </script>
 

+ 15 - 1
src/components/FormBuilderCustumComponents/QuestionsCreateAndAppend/QuestionsCreateAndAppend.vue

@@ -36,6 +36,13 @@
                    :loading="test.loading" />
         </q-td>
       </template>
+      <template #body-cell-order="props">
+        <q-td auto-width>
+          <q-input v-model="props.row.order"
+                   style="width: 80px;"
+                   :loading="test.loading" />
+        </q-td>
+      </template>
       <template #body-cell-is_extra_mark="props">
         <q-td auto-width>
           <q-toggle v-model="props.row.is_extra_mark"
@@ -58,7 +65,7 @@
                  size="sm"
                  outline
                  @click="editTestQuestion(props.row)">
-            ویرایش نمره و حالت امتیازی سوال
+            ثبت نمره و ترتیب و حالت امتیازی
           </q-btn>
           <q-btn color="red"
                  class="q-mr-md"
@@ -269,6 +276,13 @@ export default {
           align: 'left',
           field: () => ''
         },
+        {
+          name: 'order',
+          required: true,
+          label: 'ترتیب',
+          align: 'left',
+          field: row => row.order ? row.order : '-'
+        },
         {
           name: 'id',
           required: true,

+ 12 - 1
src/components/Widgets/Classroom/AdminClassroomAnswerBookList/AdminClassroomAnswerBookList.vue

@@ -124,7 +124,7 @@ export default {
           type: 'select',
           name: 'status',
           options: [
-            { label: 'جاری', value: 'WAITING_FOR_GRADING' },
+            { label: 'در انتظار تصحیح', value: 'WAITING_FOR_GRADING' },
             { label: 'در حال تصحیح', value: 'GRADING' },
             { label: 'تصحیح شده', value: 'GRADED' },
             {
@@ -138,6 +138,10 @@ export default {
             {
               label: 'اعتراض شده',
               value: 'OBJECTED'
+            },
+            {
+              label: 'پاسخ اعتراض',
+              value: 'OBJECTION_RESPONDED'
             }
           ],
           label: 'وضعیت',
@@ -240,6 +244,13 @@ export default {
           align: 'left',
           field: row => row.grader_info ? row.grader_info.firstname + ' ' + row.grader_info.lastname : '-'
         },
+        {
+          name: 'grader_info',
+          required: true,
+          label: 'زمان تصحیح',
+          align: 'left',
+          field: row => row.grading_time ? this.toShamsi(row.grading_time) : '-'
+        },
         // {
         //   name: 'last_modification_time',
         //   required: true,

+ 74 - 8
src/components/Widgets/Classroom/AdminClassroomList/AdminClassroomList.vue

@@ -58,6 +58,7 @@ 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 { Classroom } from 'src/models/Classroom.js'
 import BtnControl from 'src/components/Control/btn.vue'
 import { FormBuilderAssist } from 'quasar-form-builder'
 import { UnitCategory } from 'src/models/UnitCategory.js'
@@ -76,6 +77,15 @@ export default {
     return {
       mounted: false,
       inputs: [
+        {
+          type: 'select',
+          name: 'unit__category',
+          options: [],
+          value: null,
+          label: 'گروه آموزشی',
+          placeholder: ' ',
+          col: 'col-md-3 col-12'
+        },
         {
           type: 'select',
           name: 'unit',
@@ -83,7 +93,7 @@ export default {
           value: null,
           label: 'unit',
           placeholder: ' ',
-          col: 'col-md-2 col-12'
+          col: 'col-md-3 col-12'
         },
         {
           type: 'select',
@@ -92,10 +102,28 @@ export default {
           value: null,
           label: 'وضعیت ',
           placeholder: ' ',
-          col: 'col-md-2 col-12'
+          col: 'col-md-3 col-12'
+        },
+        {
+          type: 'select',
+          name: 'holding_type',
+          options: (new Classroom()).holding_typeEnums,
+          value: null,
+          label: 'نوع برگزاری',
+          placeholder: ' ',
+          col: 'col-md-3 col-12'
+        },
+        {
+          type: 'select',
+          name: 'professor',
+          options: [],
+          value: null,
+          label: 'professor',
+          placeholder: ' ',
+          col: 'col-md-3 col-12'
         },
         { type: 'hidden', name: 'unit__category__type', value: null },
-        { type: BtnControlComp, name: 'btn', label: 'جستجو', placeholder: ' ', atClick: () => {}, col: 'col-md-2 col-12' }
+        { type: BtnControlComp, name: 'btn', label: 'جستجو', placeholder: ' ', atClick: this.search, col: 'col-md-2 col-12' }
       ],
       defaultOptions: {
         classroomType: 'TRAINING'
@@ -214,19 +242,32 @@ export default {
         return 'Admin.Event.Show'
       }
       return 'Admin.Classroom.Show'
+    },
+    selectedUnitCategoryType () {
+      return FormBuilderAssist.getInputsByName(this.inputs, 'unit__category')?.value
+    }
+  },
+  watch: {
+    selectedUnitCategoryType () {
+      this.getUnits()
     }
   },
   mounted () {
-    this.setActionBtn()
     this.getUnits()
+    this.getProfessors()
+    this.getUnitCategories()
     this.setClassroomTypeOfInputs()
     this.mounted = true
   },
   methods: {
     setClassroomTypeOfInputs () {
       FormBuilderAssist.setAttributeByName(this.inputs, 'status', 'label', 'وضعیت ' + this.classroomTypeTitle)
+      FormBuilderAssist.setAttributeByName(this.inputs, 'professor', 'label', (this.localOptions.classroomType === 'TRAINING') ? 'استاد' : 'برگزار کننده')
       FormBuilderAssist.setAttributeByName(this.inputs, 'unit', 'label', this.getUnitFilterTitle())
       FormBuilderAssist.setAttributeByName(this.inputs, 'unit__category__type', 'value', this.localOptions.classroomType)
+      if (this.localOptions.classroomType !== 'TRAINING') {
+        this.inputs = this.inputs.filter(input => input.name !== 'unit__category')
+      }
       this.table.columns.forEach(col => {
         if (col.name === 'title') {
           col.label = 'نام ' + this.classroomTypeTitle
@@ -236,14 +277,16 @@ export default {
         }
       })
     },
-    setActionBtn () {
-      FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.search)
-    },
     search () {
       this.$refs.entityIndex.search()
     },
     getUnits () {
-      APIGateway.unit.index({ per_page: 9999, category__type: this.localOptions.classroomType })
+      const category = this.selectedUnitCategoryType
+      APIGateway.unit.index({
+        per_page: 9999,
+        category__type: this.localOptions.classroomType,
+        category
+      })
         .then((units) => {
           this.loadSelectOptions('unit', this.getSelectOptions(units.list.list, 'id', 'title'))
         })
@@ -251,6 +294,29 @@ export default {
 
         })
     },
+    getUnitCategories () {
+      APIGateway.unitCategory.index({
+        type: this.localOptions.classroomType
+      })
+        .then(unitCategoryList => {
+          this.loadSelectOptions('unit__category', this.getSelectOptions(unitCategoryList.list.list, 'id', 'title'))
+        })
+    },
+    getProfessors () {
+      APIGateway.user.professors({ per_page: 9999 })
+        .then(({ list }) => {
+          const professors = list.list.map(item => {
+            return {
+              value: item.id,
+              label: this.getUserFullname(item)
+            }
+          })
+          this.loadSelectOptions('professor', this.getSelectOptions(professors, 'value', 'label'))
+        })
+    },
+    getUserFullname (user) {
+      return user.firstname + ' ' + user.lastname
+    },
     loadSelectOptions (name, value) {
       const inputIndex = this.inputs.findIndex(input => input.name === name)
       this.inputs[inputIndex].options = value

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

@@ -95,7 +95,8 @@
       </q-tab-panel>
       <q-tab-panel name="members"
                    class="q-pa-none">
-        <members-list :classroom-id="parseInt($route.params.id)" />
+        <members-list :classroom-id="parseInt($route.params.id)"
+                      :classroom-title="this.classroom.title" />
       </q-tab-panel>
       <q-tab-panel name="teams"
                    class="q-pa-none">

+ 272 - 3
src/components/Widgets/Classroom/AdminClassroomShow/components/MembersList.vue

@@ -16,6 +16,22 @@
         خروجی اکسل
       </q-btn>
     </template>
+    <template #before-index-table>
+      <div class="row before-index-table">
+        <div class="right-side">
+          <form-builder ref="formBuilder"
+                        :key="enrollNewMembersInputsKey"
+                        v-model:value="enrollNewMembersInputs"
+                        :loading="enrollNewMembersLoading" />
+        </div>
+        <div class="left-side">
+          <form-builder ref="formBuilder"
+                        :key="registerNewMembersInputsKey"
+                        v-model:value="registerNewMembersInputs"
+                        :loading="registerNewMembersLoading" />
+        </div>
+      </div>
+    </template>
     <template v-slot:entity-index-table-cell="{inputData}">
       <template v-if="inputData.col.name === 'number'">
         {{ inputData.rowNumber }}
@@ -72,26 +88,230 @@ import { Invoice } from 'src/models/Invoice.js'
 import { APIGateway } from 'src/api/APIGateway.js'
 import { Classroom } from 'src/models/Classroom.js'
 import { Registration } from 'src/models/Registration.js'
-import { FormBuilderAssist } from 'quasar-form-builder'
+import { FormBuilder, FormBuilderAssist } from 'quasar-form-builder'
+import EntityInput from 'quasar-crud/src/components/Entity/Attachment/EntityInput.vue'
 import BtnControl from 'src/components/Control/btn.vue'
 
+const EntityInputComp = shallowRef(EntityInput)
 const BtnControlComp = shallowRef(BtnControl)
 
 export default {
   name: 'MembersList',
   components: {
-    EntityIndex
+    EntityIndex,
+    FormBuilder
   },
   props: {
     classroomId: {
       type: Number,
       default: null
+    },
+    classroomTitle: {
+      type: String,
+      default: null
     }
   },
   data () {
     const classroomId = this.classroomId
     return {
       mounted: false,
+
+      enrollNewMembersLoading: false,
+      enrollNewMembersInputsKey: Date.now(),
+      enrollNewMembersInputs: [
+        {
+          type: EntityInputComp,
+          name: 'enrollNewMembers',
+          selectionMode: 'multiple',
+          popUpButtonConfig: {
+            label: 'پیش ثبت نام مستقیم',
+            outline: true,
+            buttonColor: 'deep-purple',
+            buttonTextColor: 'white',
+            buttonBadgeColor: 'pink'
+          },
+          dialogConfirmButtonConfig: {
+            label: 'تایید',
+            buttonColor: 'deep-purple',
+            buttonTextColor: 'white',
+            buttonBadgeColor: 'pink'
+          },
+
+          apiAddress: APIGateway.user.APIAdresses.unenrolledUsersInClassroom,
+          tableTitle: 'لیست کل کاربران پیش ثبت نام نشده',
+          showTableItemsRouteName: 'Admin.BlockManagement.Show',
+          tableKeys: {
+            data: 'results',
+            total: 'count',
+            currentPage: 'current',
+            perPage: 'per_page',
+            pageKey: 'page'
+          },
+          table: {
+            columns: [
+              {
+                name: 'id',
+                required: true,
+                label: 'شناسه',
+                align: 'left',
+                field: row => row.id
+              },
+              {
+                name: 'fullname',
+                required: true,
+                label: 'نام و نام خانوادگی',
+                align: 'left',
+                field: row => row.firstname + ' ' + row.lastname
+              },
+              {
+                name: 'national_code',
+                required: true,
+                label: 'کد ملی',
+                align: 'left',
+                field: row => row.national_code
+              },
+              {
+                name: 'mobile_number',
+                required: true,
+                label: 'شماره همراه',
+                align: 'left',
+                field: row => row.mobile_number
+              },
+              {
+                name: 'last_academy_name',
+                required: true,
+                label: 'دانشگاه',
+                align: 'left',
+                field: row => row.last_academy_name
+              },
+              {
+                name: 'living_city',
+                required: true,
+                label: 'شهر',
+                align: 'left',
+                field: row => row.living_city
+              }
+            ]
+          },
+          inputs: [
+            { type: 'input', name: 'id', value: null, label: 'شناسه', placeholder: ' ', col: 'col-md-4 col-12' },
+            { type: 'input', name: 'national_code', value: null, label: 'کد ملی', placeholder: ' ', col: 'col-md-4 col-12' },
+            { type: 'input', name: 'mobile_number', value: null, label: 'شماره همراه', placeholder: ' ', col: 'col-md-4 col-12' },
+            { type: 'input', name: 'email', value: null, label: 'ایمیل', placeholder: ' ', col: 'col-md-3 col-12' },
+            { type: 'input', name: 'firstname', value: null, label: 'نام', placeholder: ' ', col: 'col-md-3 col-12' },
+            { type: 'input', name: 'lastname', value: null, label: 'نام خانوادگی', placeholder: ' ', col: 'col-md-3 col-12' },
+            { type: 'input', name: 'search', value: null, label: 'جست و جو', placeholder: ' ', col: 'col-md-3 col-12' },
+            { type: 'hidden', name: 'registered_classes__classroom__ne', value: classroomId }
+          ],
+          itemIndicatorKey: row => row.firstname + ' ' + row.lastname,
+          // itemIndicatorKey: 'firstname',
+          itemIdentifyKey: 'id',
+          onConfirmed: this.onEnrollNewMembersConfirmedSelected,
+
+          value: [],
+          selected: [],
+          col: 'col-md-12 col-12'
+        }
+      ],
+
+      registerNewMembersLoading: false,
+      registerNewMembersInputsKey: Date.now(),
+      registerNewMembersInputs: [
+        {
+          type: EntityInputComp,
+          name: 'registerNewMembers',
+          selectionMode: 'multiple',
+          popUpButtonConfig: {
+            label: 'ثبت نام مستقیم',
+            outline: true,
+            buttonColor: 'deep-purple',
+            buttonTextColor: 'white',
+            buttonBadgeColor: 'pink'
+          },
+          dialogConfirmButtonConfig: {
+            label: 'تایید',
+            buttonColor: 'deep-purple',
+            buttonTextColor: 'white',
+            buttonBadgeColor: 'pink'
+          },
+
+          apiAddress: APIGateway.user.APIAdresses.unregisteredUsersInClassroom,
+          tableTitle: 'لیست کل کاربران ثبت نام نشده',
+          showTableItemsRouteName: 'Admin.BlockManagement.Show',
+          tableKeys: {
+            data: 'results',
+            total: 'count',
+            currentPage: 'current',
+            perPage: 'per_page',
+            pageKey: 'page'
+          },
+          table: {
+            columns: [
+              {
+                name: 'id',
+                required: true,
+                label: 'شناسه',
+                align: 'left',
+                field: row => row.id
+              },
+              {
+                name: 'fullname',
+                required: true,
+                label: 'نام و نام خانوادگی',
+                align: 'left',
+                field: row => row.firstname + ' ' + row.lastname
+              },
+              {
+                name: 'national_code',
+                required: true,
+                label: 'کد ملی',
+                align: 'left',
+                field: row => row.national_code
+              },
+              {
+                name: 'mobile_number',
+                required: true,
+                label: 'شماره همراه',
+                align: 'left',
+                field: row => row.mobile_number
+              },
+              {
+                name: 'last_academy_name',
+                required: true,
+                label: 'دانشگاه',
+                align: 'left',
+                field: row => row.last_academy_name
+              },
+              {
+                name: 'living_city',
+                required: true,
+                label: 'شهر',
+                align: 'left',
+                field: row => row.living_city
+              }
+            ]
+          },
+          inputs: [
+            { type: 'input', name: 'id', value: null, label: 'شناسه', placeholder: ' ', col: 'col-md-4 col-12' },
+            { type: 'input', name: 'national_code', value: null, label: 'کد ملی', placeholder: ' ', col: 'col-md-4 col-12' },
+            { type: 'input', name: 'mobile_number', value: null, label: 'شماره همراه', placeholder: ' ', col: 'col-md-4 col-12' },
+            { type: 'input', name: 'email', value: null, label: 'ایمیل', placeholder: ' ', col: 'col-md-3 col-12' },
+            { type: 'input', name: 'firstname', value: null, label: 'نام', placeholder: ' ', col: 'col-md-3 col-12' },
+            { type: 'input', name: 'lastname', value: null, label: 'نام خانوادگی', placeholder: ' ', col: 'col-md-3 col-12' },
+            { type: 'input', name: 'search', value: null, label: 'جست و جو', placeholder: ' ', col: 'col-md-3 col-12' },
+            { type: 'hidden', name: 'registered_classes__classroom__ne', value: classroomId }
+          ],
+          itemIndicatorKey: row => row.firstname + ' ' + row.lastname,
+          // itemIndicatorKey: 'firstname',
+          itemIdentifyKey: 'id',
+          onConfirmed: this.onRegisterNewMembersConfirmedSelected,
+
+          value: [],
+          selected: [],
+          col: 'col-md-12 col-12'
+        }
+      ],
+
       createInvoiceLoading: false,
       classroom: new Classroom(),
 
@@ -203,19 +423,53 @@ export default {
     getMembersExcel () {
       this.exportReportLoading = true
       const status = FormBuilderAssist.getInputsByName(this.membersListInputs, 'status').value ? FormBuilderAssist.getInputsByName(this.membersListInputs, 'status').value : null
+      const ownerNationalCode = FormBuilderAssist.getInputsByName(this.membersListInputs, 'owner__national_code').value ? FormBuilderAssist.getInputsByName(this.membersListInputs, 'owner__national_code').value : null
+      const invoiceStatus = FormBuilderAssist.getInputsByName(this.membersListInputs, 'invoice__status').value ? FormBuilderAssist.getInputsByName(this.membersListInputs, 'invoice__status').value : null
       APIGateway.registration.exportReport({
         classroom: this.$route.params.id,
         type: 'users',
+        invoice__status: invoiceStatus,
+        owner__national_code: ownerNationalCode,
         status
       })
         .then((xlsxData) => {
-          Assist.saveXlsx(xlsxData, this.classroom.title)
+          Assist.saveXlsx(xlsxData, `اندیشه جویان ${this.classroomTitle}`)
           this.exportReportLoading = false
         })
         .catch((e) => {
           this.exportReportLoading = false
         })
     },
+    onRegisterNewMembersConfirmedSelected (data) {
+      const owners = data.map(item => item.id)
+      this.registerNewMembersLoading = true
+      APIGateway.classroom.registerNewMembers({ classroomId: this.classroomId, owners })
+        .then(() => {
+          this.registerNewMembersInputs.value = [] // does not work!
+          this.registerNewMembersInputs.selected = [] // does not work!
+          this.registerNewMembersInputsKey = Date.now()
+          this.registerNewMembersLoading = false
+          this.$refs.membersList.search()
+        })
+        .catch(() => {
+          this.registerNewMembersLoading = false
+        })
+    },
+    onEnrollNewMembersConfirmedSelected (data) {
+      const owners = data.map(item => item.id)
+      this.enrollNewMembersLoading = true
+      APIGateway.classroom.enrollNewMembers({ classroomId: this.classroomId, owners })
+        .then(() => {
+          this.enrollNewMembersInputs.value = [] // does not work!
+          this.enrollNewMembersInputs.selected = [] // does not work!
+          this.enrollNewMembersInputsKey = Date.now()
+          this.enrollNewMembersLoading = false
+          this.$refs.membersList.search()
+        })
+        .catch(() => {
+          this.enrollNewMembersLoading = false
+        })
+    },
     dropClassroomByAdmin (row) {
       const classroomId = row.classroom
       const userId = row.owner
@@ -254,3 +508,18 @@ export default {
   }
 }
 </script>
+<style lang="scss">
+.before-index-table {
+  display: flex;
+  justify-content: flex-end;
+  margin-right: 8px;
+  margin-bottom: 8px;
+}
+.right-side {
+  margin-right: 4px;
+}
+
+.left-side {
+
+}
+</style>

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

@@ -27,7 +27,7 @@
                  outline
                  color="primary"
                  label="بررسی"
-                 :to="{name: sessionAttendanceSheetList, params: {classroom_id: classroomId, session_id: inputData.props.row.id}}"
+                 :to="{name: sessionAttendanceSheetList, params: {classroom_id: classroomId, session_id: inputData.props.row.id}, query: { session_number: inputData.rowNumber }}"
                  class="q-mr-md" />
           <q-btn size="md"
                  color="primary"

+ 5 - 1
src/components/Widgets/Classroom/ClassroomAnswerBookList/ClassroomAnswerBookList.vue

@@ -65,7 +65,7 @@ export default {
           name: 'status',
           clearable: false,
           options: [
-            { label: 'جاری', value: 'WAITING_FOR_GRADING' },
+            { label: 'در انتظار تصحیح', value: 'WAITING_FOR_GRADING' },
             { label: 'در حال تصحیح', value: 'GRADING' },
             { label: 'تصحیح شده', value: 'GRADED' },
             {
@@ -79,6 +79,10 @@ export default {
             {
               label: 'اعتراض شده',
               value: 'OBJECTED'
+            },
+            {
+              label: 'پاسخ اعتراض',
+              value: 'OBJECTION_RESPONDED'
             }
           ],
           label: 'وضعیت',

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

@@ -54,7 +54,7 @@ export default {
         { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-md-6 col-12' },
         { type: 'file', name: 'thumbnail', responseKey: 'thumbnail', label: 'عکس', placeholder: ' ', col: 'col-md-6 col-12' },
         { type: 'checkbox', name: 'is_hidden', responseKey: 'is_hidden', label: 'پنهان باشد؟', placeholder: ' ', value: true, col: 'col-12' },
-        { type: ContentCategorySelectorComp, name: 'category', responseKey: 'category', col: 'col-12' },
+        { type: ContentCategorySelectorComp, name: 'category', responseKey: 'category', sendNull: true, col: 'col-12' },
         { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', col: 'col-12' },
         { type: ContentMediasComp, name: 'medias', responseKey: 'medias_info', col: 'col-12' },
         { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ایجاد محتوا', placeholder: ' ', atClick: () => {}, col: 'col-md-6' }

+ 1 - 1
src/components/Widgets/Content/AdminContentShow/AdminContentShow.vue

@@ -88,7 +88,7 @@ export default {
         { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-md-6 col-12' },
         { type: 'file', name: 'thumbnail', responseKey: 'thumbnail', label: 'عکس', placeholder: ' ', col: 'col-md-6 col-12' },
         { type: 'checkbox', name: 'is_hidden', responseKey: 'is_hidden', label: 'پنهان باشد؟', placeholder: ' ', col: 'col-12' },
-        { type: ContentCategorySelectorComp, name: 'category', responseKey: 'category_info', col: 'col-md-12 col-12' },
+        { type: ContentCategorySelectorComp, name: 'category', responseKey: 'category_info', sendNull: true, col: 'col-md-12 col-12' },
         { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', col: 'col-md-12 col-12' },
         { type: ContentMediasComp, name: 'medias', responseKey: 'medias_info', col: 'col-md-12 col-12' },
         { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ویرایش محتوا', placeholder: ' ', ignoreValue: true, atClick: this.edit, col: 'col-md-6' }

+ 2 - 6
src/components/Widgets/Content/ContentShow/ContentShow.vue

@@ -95,11 +95,7 @@ export default {
     breadcrumbsPath () {
       const path = [
         {
-          label: 'خانه',
-          to: { name: 'Public.Home' }
-        },
-        {
-          label: 'آرشیو محتوا',
+          label: 'محتوا',
           to: { name: 'Public.ContentCategoryParentParent.List' }
         }
       ]
@@ -434,7 +430,7 @@ export default {
     background: #FFFFFF;
     border: 1px solid #DFE1EC;
     border-radius: 8px;
-    padding: 36px 46px;
+    padding: 8px 8px;
   }
 }
 </style>

+ 1 - 1
src/components/Widgets/Content/ContentShow/components/MediaCategory.vue

@@ -8,7 +8,7 @@
     <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">
+           class="media-item col-md-6 col-sm-12 col-12">
         <media-item :media="media"
                     :media-type="mediaType"
                     @onView="onView(media)" />

+ 6 - 2
src/components/Widgets/Content/ContentShow/components/MediaItem.vue

@@ -12,7 +12,6 @@
         </template>
         <template v-else>
           <q-icon name="file_download" />
-          دانلود
         </template>
       </q-btn>
     </div>
@@ -56,7 +55,7 @@ export default {
   background: #F9F9F9;
   border: 1px solid #DFE1EC;
   border-radius: 10px;
-  padding: 10px 16px 10px 21px;
+  padding: 8px 4px 8px 4px;
   display: flex;
   flex-flow: row;
   justify-content: space-between;
@@ -73,5 +72,10 @@ export default {
     .q-btn {
     }
   }
+  @media screen and (min-width: 600px) {
+    & {
+      padding: 24px 48px 24px 48px;
+    }
+  }
 }
 </style>

+ 66 - 61
src/components/Widgets/ContentCategory/ContentCategoryList/ContentCategoryList.vue

@@ -5,62 +5,37 @@
     <div class="ContentCategory-info-wrapper">
       <breadcrumbs class="q-mb-xl"
                    style="margin-top: 29px; margin-bottom: 19px;" />
-      <div v-if="contentCategory.id"
-           class="ContentCategory-info">
-        <div class="ContentCategory-thumbnail">
-          <q-img :src="contentCategory.thumbnail" />
-        </div>
-        <div class="ContentCategory-meta">
-          <q-banner>
-            {{ contentCategory.title }}
-          </q-banner>
-          <div class="ContentCategory-desc"
-               v-html="contentCategory.description" />
-        </div>
-      </div>
     </div>
-    <q-banner class="ContentCategoryList-banner">
-      <span v-if="localOptions.listType === 'category_parent_parent'">
-        دسته بندی های کلی
-      </span>
-      <span v-if="localOptions.listType === 'category_parent'">
-        دسته بندی اصلی
-      </span>
-      <span v-if="localOptions.listType === 'category'">
-        دسته بندی جزئی
-      </span>
-    </q-banner>
-    <entity-index v-if="mounted"
-                  ref="entityIndex"
-                  v-model:value="inputs"
-                  :api="api"
-                  :table="table"
-                  :table-keys="tableKeys"
-                  :table-grid-size="true"
-                  :default-layout="false"
-                  :show-search-button="false"
-                  :show-expand-button="false"
-                  :show-reload-button="false">
-      <template #entity-index-table-item-cell="{inputData}">
-        <content-category-item :content-category="getContentCategory(inputData.props.row)"
-                               :show-category-route-name="showCategoryRouteName" />
+    <filters :show-category-route-name="showCategoryRouteName"
+             :content-categories="contentCategories"
+             @filter="onChangeFilter" />
+    <div class="contents row">
+      <q-linear-progress v-if="contents.loading"
+                         class="q-my-md"
+                         indeterminate />
+      <template v-else>
+        <div v-for="(content, contentIndex) in contents.list"
+             :key="contentIndex"
+             class="col-md-4 col-12">
+          <content-item :content="content" />
+        </div>
       </template>
-    </entity-index>
+    </div>
   </div>
 </template>
 
 <script>
-import { EntityIndex } from 'quasar-crud'
+import Filters from './filters.vue'
+import ContentItem from './ContentItem.vue'
 import { mixinWidget } from 'src/mixin/Mixins.js'
 import { APIGateway } from 'src/api/APIGateway.js'
-import { FormBuilderAssist } from 'quasar-form-builder'
-import ContentCategoryItem from './ContentCategoryItem.vue'
-import { ContentCategory } from 'src/models/ContentCategory.js'
+import { ContentList } from 'src/models/Content.js'
 import Breadcrumbs from 'src/components/Widgets/Breadcrumbs/Breadcrumbs.vue'
+import { ContentCategory, ContentCategoryList } from 'src/models/ContentCategory.js'
 
 export default {
   name: 'ContentCategoryList',
-  components: { ContentCategoryItem, EntityIndex, Breadcrumbs },
+  components: { Breadcrumbs, Filters, ContentItem },
   mixins: [mixinWidget],
   data () {
     return {
@@ -80,6 +55,9 @@ export default {
         columns: []
       },
       mounted: false,
+      subFilterExpanded: false,
+      contents: new ContentList(),
+      contentCategories: new ContentCategoryList(),
       contentCategory: new ContentCategory(),
       contentCategoryParent: new ContentCategory(),
       contentCategoryParentParent: new ContentCategory(),
@@ -102,11 +80,7 @@ export default {
     breadcrumbsPath () {
       const path = [
         {
-          label: 'خانه',
-          to: { name: 'Public.Home' }
-        },
-        {
-          label: 'آرشیو محتوا',
+          label: 'محتوا',
           to: { name: 'Public.ContentCategoryParentParent.List' }
         }
       ]
@@ -144,21 +118,13 @@ export default {
   },
   created() {
     this.updateBreadcrumbs()
-
-    if (this.localOptions.listType === 'category_parent_parent') {
-      FormBuilderAssist.setAttributeByName(this.inputs, 'parent__isnull', 'value', true)
-    } else if (this.localOptions.listType === 'category_parent') {
-      FormBuilderAssist.setAttributeByName(this.inputs, 'parent__isnull', 'value', null)
-      FormBuilderAssist.setAttributeByName(this.inputs, 'parent', 'value', this.contentCategoryId)
-    } else if (this.localOptions.listType === 'category') {
-      FormBuilderAssist.setAttributeByName(this.inputs, 'parent__isnull', 'value', null)
-      FormBuilderAssist.setAttributeByName(this.inputs, 'parent', 'value', this.contentCategoryId)
-    }
+  },
+  mounted () {
     if (this.contentCategoryId) {
       this.loadContentCategory()
     }
-  },
-  mounted () {
+    this.getContentCategories()
+    this.getContents({ selected_category: this.contentCategoryId })
     this.mounted = true
   },
   methods: {
@@ -207,6 +173,45 @@ export default {
       }
 
       this.$store.commit('AppLayout/updateBreadcrumbs', breadcrumbs)
+    },
+
+    getFilterOfCategorySearch () {
+      const filter = {}
+      if (this.localOptions.listType === 'category_parent_parent') {
+        filter.parent__isnull = true
+      } else if (this.localOptions.listType === 'category_parent') {
+        filter.parent__isnull = null
+        filter.parent = this.contentCategoryId
+      } else if (this.localOptions.listType === 'category') {
+        filter.parent__isnull = null
+        filter.parent = this.contentCategoryId
+      }
+
+      return filter
+    },
+    getContents (filters) {
+      this.contents.loading = true
+      APIGateway.content.index({ ...filters, per_page: 9999 })
+        .then(({ list }) => {
+          this.contents = new ContentList(list)
+        })
+        .finally(() => {
+          this.contents.loading = false
+        })
+    },
+    getContentCategories () {
+      const filter = this.getFilterOfCategorySearch()
+      this.contentCategories.loading = true
+      APIGateway.contentCategory.index({ ...filter, per_page: 9999 })
+        .then(({ list }) => {
+          this.contentCategories = new ContentCategoryList(list)
+        })
+        .finally(() => {
+          this.contentCategories.loading = false
+        })
+    },
+    onChangeFilter (newFilter) {
+      this.getContents({ ...newFilter, selected_category: this.contentCategoryId })
     }
   }
 }

+ 78 - 0
src/components/Widgets/ContentCategory/ContentCategoryList/ContentItem.vue

@@ -0,0 +1,78 @@
+<template>
+  <q-card class="ContentItem"
+          flat>
+    <div class="thumbnail"
+         @click="goToContentPageShow">
+      <q-img :src="content.thumbnail" />
+    </div>
+    <div class="title ellipsis-2-lines"
+         @click="goToContentPageShow">
+      {{ content.title }}
+    </div>
+    <content-item-categories :content="content" />
+    <q-btn color="primary"
+           class="show-content-btn"
+           :to="{name: 'Public.Content.Show', params: {id: content.id}}">
+      مشاهده
+    </q-btn>
+  </q-card>
+</template>
+
+<script>
+import { Content } from 'src/models/Content.js'
+import ContentItemCategories from './ContentItemCategories.vue'
+
+export default {
+  name: 'ContentItem',
+  components: { ContentItemCategories },
+  props: {
+    content: {
+      type: Content,
+      default: new Content()
+    }
+  },
+  methods: {
+    goToContentPageShow () {
+      this.$router.push({ name: 'Public.Content.Show', params: { id: this.content.id } })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.ContentItem {
+  background-color: #f6f6f6;
+  display: flex;
+  flex-flow: column;
+  position: relative;
+  cursor: pointer;
+  padding: 16px;
+  margin: 8px;
+  border-radius: 16px;
+  .thumbnail {
+    margin-bottom: 16px;
+    .q-img {
+      width: 100%;
+      border-radius: 8px;
+    }
+  }
+  .title {
+    text-align: center;
+    margin-bottom: 16px;
+    font-size: 16px;
+  }
+  .description {
+    position: absolute;
+    left: 21px;
+    color: #FFFFFF;
+    font-size: 16px;
+    display: none;
+  }
+  .show-content-btn {
+    width: 100%;
+    border-top-right-radius: 0;
+    border-top-left-radius: 0;
+    height: 60px;
+  }
+}
+</style>

+ 102 - 0
src/components/Widgets/ContentCategory/ContentCategoryList/ContentItemCategories.vue

@@ -0,0 +1,102 @@
+<template>
+  <div class="ContentItemCategories">
+    <template v-if="content?.category_info?.parent?.parent?.id">
+      <q-btn v-if="content?.category_info?.parent?.parent?.id"
+             flat
+             :to="{ name: 'Public.ContentCategoryParentParent.Show', params: { category_id: content.category_info.parent.parent.id } }"
+             class="category-btn">
+        <div class="category-btn-title q-px-sm">
+          {{ content.category_info.parent.parent.title }}
+        </div>
+      </q-btn>
+      <q-btn v-if="content?.category_info?.parent?.id"
+             flat
+             :to="{ name: 'Public.ContentCategoryParent.Show', params: { category_id: content.category_info.parent.id } }"
+             class="category-btn">
+        <div class="category-btn-title q-px-sm">
+          {{ content.category_info.parent.title }}
+        </div>
+      </q-btn>
+      <q-btn v-if="content?.category_info?.id"
+             flat
+             :to="{ name: 'Public.ContentCategory.Show', params: { category_id: content.category_info.id } }"
+             class="category-btn">
+        <div class="category-btn-title q-px-sm">
+          {{ content.category_info.title }}
+        </div>
+      </q-btn>
+    </template>
+    <template v-else-if="content?.category_info?.parent?.id">
+      <q-btn flat
+             :to="{ name: 'Public.ContentCategoryParentParent.Show', params: { category_id: content.category_info.parent.id } }"
+             class="category-btn">
+        <div class="category-btn-title q-px-sm">
+          {{ content.category_info.parent.title }}
+        </div>
+      </q-btn>
+      <q-btn v-if="content?.category_info?.id"
+             flat
+             :to="{ name: 'Public.ContentCategoryParent.Show', params: { category_id: content.category_info.id } }"
+             class="category-btn">
+        <div class="category-btn-title q-px-sm">
+          {{ content.category_info.title }}
+        </div>
+      </q-btn>
+    </template>
+    <template v-else-if="content?.category_info?.id">
+      <q-btn flat
+             :to="{ name: 'Public.ContentCategoryParentParent.Show', params: { category_id: content.category_info.id } }"
+             class="category-btn">
+        <div class="category-btn-title q-px-sm">
+          {{ content.category_info.title }}
+        </div>
+      </q-btn>
+    </template>
+  </div>
+</template>
+
+<script>
+import { Content } from 'src/models/Content.js'
+
+export default {
+  name: 'ContentItemCategories',
+  props: {
+    content: {
+      type: Content,
+      default: new Content()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.ContentItemCategories {
+  display: flex;
+  gap: 4px;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: flex-start;
+  margin-bottom: 16px;
+  height: 76px;
+  .category-btn {
+    border: 1px solid #000000;
+    border-radius: 20px;
+    padding: 4px;
+    display: flex;
+    justify-content: space-between;
+    height: 36px;
+    align-items: center;
+    background: transparent;
+    .category-btn-title {
+      font-size: 12px;
+      color: #212121;
+    }
+    &.category-btn__selected {
+      background: #444444;
+      .category-btn-title {
+        color: white;
+      }
+    }
+  }
+}
+</style>

+ 200 - 0
src/components/Widgets/ContentCategory/ContentCategoryList/filters.vue

@@ -0,0 +1,200 @@
+<template>
+  <div class="filters">
+    <div class="main-filters">
+      <div class="contentCategories">
+        <q-btn v-if="contentCategories.list.length > 0"
+               flat
+               class="filter-btn contentCategory filter-btn__selected">
+          <div class="filter-btn-title">
+            همه
+          </div>
+        </q-btn>
+        <q-btn v-for="(contentCategory, contentCategoryIndex) in contentCategories.list"
+               :key="contentCategoryIndex"
+               flat
+               :to="{name: showCategoryRouteName, params: {category_id: contentCategory.id}}"
+               class="filter-btn contentCategory">
+          <div class="filter-btn-title">
+            {{ contentCategory.title }}
+          </div>
+        </q-btn>
+      </div>
+      <div class="content-type-filters">
+        <q-btn v-if="screenGtMd"
+               flat
+               class="filter-btn"
+               :class="{'filter-btn__selected': subFilterExpanded}"
+               @click="subFilterExpanded = !subFilterExpanded">
+          <div class="filter-btn-title q-px-sm">
+            <q-icon name="filter_alt" />
+          </div>
+        </q-btn>
+      </div>
+    </div>
+    <div class="sub-filters">
+      <q-expansion-item v-model="subFilterExpanded"
+                        :header-style="'display: none;'">
+        <q-card class="sub-filters-card">
+          <div class="media-types">
+            <q-btn v-for="(mediaType, mediaTypeIndex) in mediaTypes"
+                   :key="mediaTypeIndex"
+                   flat
+                   class="filter-btn contentCategory"
+                   :class="{'filter-btn__selected': selectedMediaType === mediaType.value}"
+                   @click="filterMediaType(mediaType.value)">
+              <div class="filter-btn-title">
+                {{ mediaType.label }}
+              </div>
+            </q-btn>
+          </div>
+          <div class="search-input">
+            <q-input v-model="searchText"
+                     debounce="500">
+              <template #prepend>
+                <q-icon name="search" />
+              </template>
+            </q-input>
+          </div>
+        </q-card>
+      </q-expansion-item>
+    </div>
+  </div>
+</template>
+
+<script>
+import { Media } from 'src/models/Media.js'
+import { ContentCategory, ContentCategoryList } from 'src/models/ContentCategory.js'
+
+export default {
+  name: 'ContentCategoryItem',
+  props: {
+    contentCategories: {
+      type: ContentCategoryList,
+      default: new ContentCategoryList()
+    },
+    contentCategory: {
+      type: ContentCategory,
+      default: new ContentCategory()
+    },
+    showCategoryRouteName: {
+      type: String,
+      default: 'Public.ContentCategory.Show'
+    },
+    listType: {
+      type: String,
+      default: 'category_parent_parent'
+    }
+  },
+  emits: ['filter'],
+  data () {
+    return {
+      media: new Media(),
+      searchText: null,
+      selectedMediaType: null,
+      subFilterExpanded: false
+    }
+  },
+  computed: {
+    mediaTypes () {
+      return [{
+        label: 'همه',
+        value: null
+      }].concat(this.media.typeEnums)
+    },
+    screenGtMd () {
+      if (typeof window === 'undefined') {
+        return true
+      }
+      return this.$q.screen.gt.md
+    }
+  },
+  watch: {
+    searchText () {
+      this.emitFilters()
+    },
+    screenGtMd: {
+      handler (newValue) {
+        if (!newValue) {
+          this.$nextTick(() => {
+            this.subFilterExpanded = true
+          })
+        }
+      },
+      immediate: true
+    }
+  },
+  methods: {
+    filterMediaType (mediaType) {
+      this.selectedMediaType = mediaType
+      this.emitFilters()
+    },
+    emitFilters () {
+      this.$emit('filter', this.getFilterObject())
+    },
+    getFilterObject () {
+      return {
+        medias__type: this.selectedMediaType,
+        search: this.searchText
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.filters {
+  .main-filters {
+    display: flex;
+    gap: 8px;
+    flex-wrap: wrap;
+    justify-content: space-between;
+
+    .contentCategories {
+      display: flex;
+      gap: 8px;
+      flex-wrap: wrap;
+      justify-content: flex-start;
+    }
+    .content-type-filters {
+
+    }
+  }
+  .sub-filters {
+    .sub-filters-card {
+      display: flex;
+      gap: 8px;
+      flex-wrap: wrap;
+      align-items: center;
+      justify-content: space-between;
+      padding-top: 12px;
+      .media-types {
+        display: flex;
+        gap: 8px;
+        flex-wrap: wrap;
+        justify-content: flex-start;
+        height: 42.63px;
+      }
+    }
+  }
+  .filter-btn {
+    border: 1px solid #000000;
+    border-radius: 20px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    background: #FFFFFF;
+    height: 43px;
+    padding: 0 8px;
+    .filter-btn-title {
+      font-size: 14px;
+      color: #212121;
+    }
+    &.filter-btn__selected {
+      background: #444444;
+      .filter-btn-title {
+        color: white;
+      }
+    }
+  }
+}
+</style>

+ 55 - 56
src/components/Widgets/ContentCategory/ContentCategoryShow/ContentCategoryShow.vue

@@ -5,19 +5,6 @@
     <div class="ContentCategory-info-wrapper">
       <breadcrumbs class="q-mb-xl"
                    style="margin-top: 29px; margin-bottom: 19px;" />
-      <div v-if="contentCategory.id"
-           class="ContentCategory-info">
-        <div class="ContentCategory-thumbnail">
-          <q-img :src="contentCategory.thumbnail" />
-        </div>
-        <div class="ContentCategory-meta">
-          <q-banner>
-            {{ contentCategory.title }}
-          </q-banner>
-          <div class="ContentCategory-desc"
-               v-html="contentCategory.description" />
-        </div>
-      </div>
     </div>
     <q-banner class="ContentCategoryShow-banner">
       محتواهای
@@ -26,7 +13,7 @@
     <entity-index v-if="mounted && contentCategory.id"
                   ref="entityIndex"
                   v-model:value="contentInputs"
-                  class="product-index"
+                  class="content-index"
                   :api="contentApi"
                   :table="table"
                   :table-keys="tableKeys"
@@ -39,26 +26,29 @@
         <content-item :content="getContent(inputData.props.row)" />
       </template>
     </entity-index>
-    <q-banner class="ContentCategoryShow-banner">
-      محصولات
-      {{ contentCategory.title }}
+    <q-banner v-if="showRelatedProduct"
+              class="ContentCategoryShow-banner">
+      محصولات مرتبط
     </q-banner>
-    <entity-index v-if="mounted && contentCategory.id"
-                  ref="entityIndex"
-                  v-model:value="productInputs"
-                  class="product-index"
-                  :api="productApi"
-                  :table="table"
-                  :table-keys="tableKeys"
-                  :table-grid-size="true"
-                  :default-layout="false"
-                  :show-search-button="false"
-                  :show-expand-button="false"
-                  :show-reload-button="false">
-      <template #entity-index-table-item-cell="{inputData}">
-        <product-item :product="getProduct(inputData.props.row)" />
-      </template>
-    </entity-index>
+    <div class="product-index-wrapper">
+      <entity-index v-if="mounted && contentCategory.id && showRelatedProduct"
+                    ref="entityIndex"
+                    v-model:value="productInputs"
+                    class="product-index"
+                    :api="productApi"
+                    :table="table"
+                    :table-keys="tableKeys"
+                    :table-grid-size="true"
+                    :default-layout="false"
+                    :show-search-button="false"
+                    :show-expand-button="false"
+                    :show-reload-button="false"
+                    @onPageChanged="onProductPageChanged">
+        <template #entity-index-table-item-cell="{inputData}">
+          <product-item :product="getProduct(inputData.props.row)" />
+        </template>
+      </entity-index>
+    </div>
   </div>
 </template>
 
@@ -101,6 +91,7 @@ export default {
         columns: []
       },
       mounted: false,
+      showRelatedProduct: true,
       contentCategory: new ContentCategory(),
       contentCategoryParent: new ContentCategory(),
       contentCategoryParentParent: new ContentCategory(),
@@ -123,11 +114,7 @@ export default {
     breadcrumbsPath () {
       const path = [
         {
-          label: 'خانه',
-          to: { name: 'Public.Home' }
-        },
-        {
-          label: 'آرشیو محتوا',
+          label: 'محتوا',
           to: { name: 'Public.ContentCategoryParentParent.List' }
         }
       ]
@@ -285,6 +272,10 @@ export default {
       }
 
       this.$store.commit('AppLayout/updateBreadcrumbs', breadcrumbs)
+    },
+    onProductPageChanged (response) {
+      console.log(response.data)
+      this.showRelatedProduct = response?.data?.results?.length !== 0
     }
   }
 }
@@ -296,29 +287,37 @@ export default {
     margin-bottom: 39px;
     margin-top: 50px;
   }
-  :deep(.entity-index) {
-    margin-top: 24px;
-    .quasar-crud-index-table {
-      .q-table__top {
-        display: none;
-      }
-      .q-table__grid-content {
-        $gap: 24px;
-        flex-wrap: wrap;
-        gap: $gap;
-        justify-content: center;
-        .product-item {
-          flex: 0 0 calc( 25% - #{$gap} );
-          box-sizing: border-box;
-        }
-      }
-      .q-table__bottom {
-        .q-table__control {
+
+  .product-index-wrapper {
+    :deep(.entity-index) {
+      margin-top: 24px;
+
+      .quasar-crud-index-table {
+        .q-table__top {
           display: none;
         }
+
+        .q-table__grid-content {
+          $gap: 24px;
+          flex-wrap: wrap;
+          gap: $gap;
+          //justify-content: center;
+
+          .product-item {
+            flex: 0 0 calc(25% - #{$gap});
+            box-sizing: border-box;
+          }
+        }
+
+        .q-table__bottom {
+          .q-table__control {
+            display: none;
+          }
+        }
       }
     }
   }
+
   .ContentCategory-info-wrapper {
     background: white;
     width: 100%;

+ 2 - 1
src/components/Widgets/Event/AdminEventShow/AdminEventShow.vue

@@ -39,7 +39,8 @@
       </q-tab-panel>
       <q-tab-panel name="members"
                    class="q-pa-none">
-        <members-list :event-id="$route.params.id" />
+        <members-list :event-id="$route.params.id"
+                      :classroom-title="this.classroom.title" />
       </q-tab-panel>
       <q-tab-panel name="live_streaming_url">
         <live-streaming :event="event" />

+ 41 - 17
src/components/Widgets/ProjectAttendanceSheet/AdminProjectAttendanceSheetShow/AdminProjectAttendanceSheetShow.vue

@@ -7,8 +7,8 @@
       >
     </q-btn>
   </div>
-  <entity-show v-if="mounted"
-               ref="entityShow"
+  <entity-edit v-if="mounted"
+               ref="entityEdit"
                v-model:value="inputs"
                title="جزییات پروژه"
                :api="api"
@@ -21,11 +21,16 @@
                :after-load-input-data="afterLoadInputData">
     <template #after-form-builder>
       <div class="flex justify-end q-mt-md">
+        <q-btn color="secondary"
+               label="ثبت توضیحات"
+               :loading="loading"
+               class="q-mr-sm"
+               @click="saveNotation" />
         <q-btn color="red"
                outline
                label="انجام نشده"
                :loading="loading"
-               class="q-mr-md"
+               class="q-mr-sm"
                @click="notVerify" />
         <q-btn color="primary"
                label="انجام شده"
@@ -33,23 +38,24 @@
                @click="verify" />
       </div>
     </template>
-  </entity-show>
+  </entity-edit>
 </template>
 
 <script>
 import { shallowRef } from 'vue'
-import { EntityShow } from 'quasar-crud'
+import { EntityEdit } from 'quasar-crud'
 import { Project } from 'src/models/Project.js'
 import { APIGateway } from 'src/api/APIGateway.js'
 import { ProjectAttendanceSheets } from 'src/models/ProjectAttendanceSheets.js'
 import ContentsSelector from 'src/components/FormBuilderCustumComponents/ContentsSelector/ContentsSelector.vue'
+import { FormBuilderAssist } from 'quasar-form-builder'
 
 const ContentsSelectorComp = shallowRef(ContentsSelector)
 
 export default {
   name: 'AdminProjectAttendanceSheetShow',
   components: {
-    EntityShow
+    EntityEdit
   },
   data () {
     // const projectId = this.$route.params.project_id
@@ -61,11 +67,11 @@ export default {
       api: APIGateway.projectAttendanceSheets.APIAdresses.byId(projectAttendanceSheetsId),
       inputs: [
         { type: 'separator', name: 'space', label: 'مشخصات اندیشه جو', className: 'custom-separator', col: 'col-12' },
-        { type: 'input', name: 'owner_info.firstname', responseKey: 'owner_info.firstname', label: 'نام', placeholder: ' ', col: 'col-md-6 col-12' },
-        { type: 'input', name: 'owner_info.lastname', responseKey: 'owner_info.lastname', label: 'نام خانوادگی', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'input', name: 'owner_info.firstname', responseKey: 'owner_info.firstname', label: 'نام', placeholder: ' ', col: 'col-md-6 col-12', readonly: true },
+        { type: 'input', name: 'owner_info.lastname', responseKey: 'owner_info.lastname', label: 'نام خانوادگی', placeholder: ' ', col: 'col-md-6 col-12', readonly: true },
 
         { type: 'separator', name: 'space', label: 'مشخصات پروژه', className: 'custom-separator', col: 'col-12' },
-        { type: 'input', name: 'project_info.title', responseKey: 'project_info.title', label: 'عنوان پروژه', placeholder: ' ', col: 'col-md-9 col-12' },
+        { type: 'input', name: 'project_info.title', responseKey: 'project_info.title', label: 'عنوان پروژه', placeholder: ' ', col: 'col-md-9 col-12', readonly: true },
         {
           type: 'select',
           name: 'project_info.type',
@@ -73,13 +79,14 @@ export default {
           label: 'نوع پروژه',
           options: (new Project()).typeEnums,
           placeholder: ' ',
-          col: 'col-md-3 col-12'
+          col: 'col-md-3 col-12',
+          readonly: true
         },
-        { type: 'inputEditor', name: 'project_info.description', responseKey: 'project_info.description', label: 'توضیحات پروژه', placeholder: ' ', col: 'col-12' },
+        { type: 'inputEditor', name: 'project_info.description', responseKey: 'project_info.description', label: 'توضیحات پروژه', placeholder: ' ', col: 'col-12', readonly: true },
 
         { type: 'separator', name: 'space', label: 'پاسخ اندیشه چو', className: 'custom-separator', col: 'col-12' },
-        { type: 'inputEditor', name: 'answer_text', responseKey: 'answer_text', label: 'پاسخ کاربر', placeholder: ' ', col: 'col-12' },
-        { type: 'file', name: 'answer_attachment', responseKey: 'answer_attachment', label: 'فایل ضمیمه کاربر', placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'inputEditor', name: 'answer_text', responseKey: 'answer_text', label: 'پاسخ کاربر', placeholder: ' ', col: 'col-12', readonly: true },
+        { type: 'file', name: 'answer_attachment', responseKey: 'answer_attachment', label: 'فایل ضمیمه کاربر', placeholder: ' ', col: 'col-md-3 col-12', readonly: true },
         {
           type: 'select',
           name: 'answer_status',
@@ -87,7 +94,8 @@ export default {
           label: 'وضعیت',
           options: (new ProjectAttendanceSheets()).answer_statusEnums,
           placeholder: ' ',
-          col: 'col-md-3 col-12'
+          col: 'col-md-3 col-12',
+          readonly: true
         },
         {
           type: 'select',
@@ -96,12 +104,14 @@ export default {
           label: 'نتیجه',
           options: (new ProjectAttendanceSheets()).is_answer_verifiedEnums,
           placeholder: ' ',
-          col: 'col-md-3 col-12'
+          col: 'col-md-3 col-12',
+          readonly: true
         },
         { type: 'separator', name: 'space', size: '0', col: 'col-md-12' },
 
-        { type: ContentsSelectorComp, name: 'project_info.contents_info', responseKey: 'project_info.contents_info', col: 'col-12' },
-        { type: 'inputEditor', name: 'project_info.correct_answer', responseKey: 'project_info.correct_answer', label: 'پاسخ صحیح', placeholder: ' ', col: 'col-12' }
+        { type: ContentsSelectorComp, name: 'project_info.contents_info', responseKey: 'project_info.contents_info', col: 'col-12', readonly: true },
+        { type: 'inputEditor', name: 'project_info.correct_answer', responseKey: 'project_info.correct_answer', label: 'پاسخ صحیح', placeholder: ' ', col: 'col-12', readonly: true },
+        { type: 'inputEditor', name: 'notation', responseKey: 'notation', label: 'توضیحات تصحیح کننده', placeholder: ' ', col: 'col-12' }
       ]
     }
   },
@@ -140,6 +150,20 @@ export default {
           this.loading = false
           this.reloadData()
         })
+    },
+    saveNotation () {
+      this.loading = true
+      const notation = FormBuilderAssist.getInputsByName(this.inputs, 'notation')?.value
+      APIGateway.projectAttendanceSheets.saveNotation(this.$route.params.project_attendance_sheets_id, notation)
+        .then(() => {
+          this.loading = false
+          this.reloadData()
+          // this.$router.go(-1)
+        })
+        .catch(() => {
+          this.loading = false
+          this.reloadData()
+        })
     }
   }
 }

+ 2 - 1
src/components/Widgets/SessionAttendanceSheet/AdminSessionAttendanceSheetList/AdminSessionAttendanceSheetList.vue

@@ -285,7 +285,8 @@ export default {
         registration__status: status
       })
         .then((xlsxData) => {
-          Assist.saveXlsx(xlsxData, this.classroom.title)
+          const title = `جلسه  ${this.$route.query.session_number} - ${this.classroom.title}`
+          Assist.saveXlsx(xlsxData, title)
           this.exportReportLoading = false
         })
         .catch((e) => {

+ 1 - 1
src/components/Widgets/Store/Shop/components/productItem.vue

@@ -53,7 +53,7 @@
         مشاهده جزییات
       </q-btn>
       <template v-if="!product.is_add_to_basket_disabled">
-        <q-btn v-if="!basketItem && product.inventory > 0"
+        <q-btn v-if="!basketItem && (product.inventory > 0 || product.inventory ===-1)"
                class="product-item__actions-add-to-cart"
                :loading="addToCartLoading"
                color="primary"

+ 1 - 1
src/components/Widgets/Store/StorePackage/StorePackageShow/StorePackageShow.vue

@@ -49,7 +49,7 @@
         </div>
         <div class="package-show__summery-price-action">
           <template v-if="!storePackage.is_add_to_basket_disabled">
-            <q-btn v-if="!basketItem && storePackage.inventory > 0"
+            <q-btn v-if="!basketItem && (storePackage.inventory > 0 || storePackage.inventory === -1)"
                    class="package-btn-add-to-cart"
                    :loading="addToCartLoading"
                    color="primary"

+ 1 - 1
src/components/Widgets/Store/StoreProduct/ProductCarousel/productItem.vue

@@ -53,7 +53,7 @@
         مشاهده جزییات
       </q-btn>
       <template v-if="!product.is_add_to_basket_disabled">
-        <q-btn v-if="!basketItem && product.inventory > 0"
+        <q-btn v-if="!basketItem && (product.inventory > 0 || product.inventory ===-1)"
                class="product-item__actions-add-to-cart"
                :loading="addToCartLoading"
                color="primary"

+ 1 - 1
src/components/Widgets/Store/StoreProduct/StoreProductShow/StoreProductShow.vue

@@ -73,7 +73,7 @@
         </div>
         <div class="product-show__summery-price-action">
           <template v-if="!product.is_add_to_basket_disabled">
-            <q-btn v-if="!basketItem && product.inventory > 0"
+            <q-btn v-if="!basketItem && (product.inventory > 0 || product.inventory ===-1)"
                    class="product-btn-add-to-cart"
                    :loading="addToCartLoading"
                    color="primary"

+ 1 - 1
src/components/Widgets/Store/StoreProduct/StoreRelatedProducts/components/productItem.vue

@@ -53,7 +53,7 @@
       </div>
       <div class="product-item__price-action">
         <template v-if="!product.is_add_to_basket_disabled">
-          <q-btn v-if="!basketItem && product.inventory > 0"
+          <q-btn v-if="!basketItem && (product.inventory > 0 || product.inventory ===-1)"
                  class="product-btn-add-to-cart"
                  :loading="addToCartLoading"
                  color="primary"

+ 207 - 18
src/components/Widgets/Test/AdminAnswerBookShow/AdminAnswerBookShow.vue

@@ -4,6 +4,13 @@
     <q-card v-if="!answerBook.loading && answerBook.id">
       <q-card-section class="flex justify-between">
         <div>{{ answerBook.test_info.title }}</div>
+        <q-btn v-if="!answerBook.loading && answerBook.ticket"
+               outline
+               color="primary"
+               class="btn-need-installment"
+               @click="scrollToObjectionBox">
+          مشاهده اعتراض
+        </q-btn>
       </q-card-section>
       <q-separator />
       <q-banner>
@@ -44,32 +51,32 @@
           </div>
         </div>
       </q-banner>
+      <q-btn v-if="!answerBook.grader && $route.query.read_only !== '1'"
+             color="primary"
+             class="full-width"
+             @click="setGraderToMe">
+        تصحیح توسط شما
+      </q-btn>
+      <q-separator />
       <q-list separator>
         <q-item v-for="(answerSheet, answerSheetIndex) in answerBook.answer_sheet_info.list"
                 :key="answerSheetIndex"
                 clickable>
           <div class="row full-width q-col-gutter-md">
-            <div class="col-12"
-                 :class="{'col-md-10': $route.query.classroom_type !== 'EVENT'}">
+            <div class="col-md-10 col-12">
               <div v-html="'<span>' + (answerSheetIndex + 1) + ' - </span>' + answerSheet.test_question_info.question_info.text" />
               <q-card class="q-mt-sm">
-                <q-card-section v-if="Array.isArray(answerSheet.test_question_info.question_info.choices_info) && answerSheet.test_question_info.question_info.choices_info.length > 0">
-                  <div v-for="(choice, choiceIndex) in answerSheet.test_question_info.question_info.choices_info"
-                       :key="choiceIndex">
-                    <q-banner>
-                      <template v-slot:avatar>
-                        <q-icon :name="answerSheet.answer_choice_index === choiceIndex ? 'check_box' : 'check_box_outline_blank'"
-                                size="sm"
-                                color="primary" />
-                      </template>
-                      <div v-html="choice.text.replace(/\n/g, '<br>')" />
-                    </q-banner>
-                    <q-separator />
+                <q-card-section>
+                  <div>
+                    پاسخ صحیح:
                   </div>
+                  <div v-html="answerSheet.test_question_info.question_info.correct_answer" />
                 </q-card-section>
-                <q-card-section v-else>
+              </q-card>
+              <q-card class="q-mt-sm">
+                <q-card-section>
                   <div>
-                    متن پاسخ:
+                    متن پاسخ کاربر:
                   </div>
                   <div>
                     {{ answerSheet.answer_text }}
@@ -79,7 +86,7 @@
                     <q-btn outline
                            color="primary"
                            icon="attachment"
-                           label="فایل پیوست"
+                           label="فایل پیوست کاربر"
                            :href="answerSheet.answer_attachment"
                            target="_blank" />
                   </div>
@@ -91,7 +98,7 @@
               <q-badge v-if="answerSheet.score !== null">
                 {{ answerSheet.score }}
                 از
-                {{ answerSheet.test_question_info.question_info.mark }}
+                {{ answerSheet.test_question_info.mark }}
                 نمره
               </q-badge>
               <q-badge v-else>
@@ -165,11 +172,89 @@
         </div>
         <div v-html="answerBook.objection_result" />
       </template>
+      <q-banner>
+        <q-input v-model="answerBook.grader_description"
+                 type="textarea"
+                 label="توضیحات مصحح"
+                 :loading="graderDescriptionLoading" />
+        <div class="row justify-end q-py-md">
+          <q-btn color="primary"
+                 label="ثبت توضیحات مصحح"
+                 outline
+                 :loading="graderDescriptionLoading"
+                 @click="submitGraderDescription" />
+        </div>
+      </q-banner>
+      <q-card-actions v-if="['GRADING', 'UNGRADABLE', 'OBJECTED', 'OBJECTION_RESPONDED'].includes(answerBook.status)">
+        <q-btn color="primary"
+               class="q-mr-lg"
+               @click="confirmScores">
+          تایید نهایی نمرات
+        </q-btn>
+        <q-btn v-if="answerBook.status === 'GRADING'"
+               color="red"
+               @click="confirmUngradable">
+          نیاز به رفع مشکل
+        </q-btn>
+      </q-card-actions>
     </q-card>
     <div v-else>
       کمی صبر کنید...
     </div>
   </div>
+  <q-card v-if="mounted && !answerBook.loading && answerBook.id && answerBook.ticket"
+          ref="objectionBox"
+          class="q-mt-md">
+    <q-card-section>
+      <div class="row justify-center">
+        <div class="full-width">
+          <q-skeleton v-if="entityLoading"
+                      type="rect"
+                      width="100%"
+                      height="200px" />
+          <template v-else>
+            {{ answerBook.ticket_info.body }}
+            <q-chat-message v-for="message in answerBook.ticket_info.replies_info.list"
+                            :key="message.id"
+                            :avatar="message.creator_info?.picture ? message.creator_info.picture : 'assets/images/web/sample-avatar.svg'"
+                            :name="message.creator_info?.firstname + ' ' + message.creator_info?.lastname"
+                            text-html
+                            :text="[message.body]"
+                            :sent="!isUserMessage(this.user, message)">
+              <template v-slot:stamp>
+                <div>
+                  {{ miladiToShamsi(message.creation_time) }}
+                </div>
+                <q-btn icon="clear"
+                       round
+                       color="red"
+                       flat
+                       size="xs"
+                       @click="removeMessage(message.id)" />
+                <q-icon v-if="isUserMessage(this.user, message)"
+                        :name="message.read ? 'done_all' : 'done'" />
+              </template>
+            </q-chat-message>
+            <div v-if="['OBJECTED', 'OBJECTION_RESPONDED'].includes(answerBook.status)">
+              <q-input v-model="replyText"
+                       bottom-slots
+                       autogrow
+                       :loading="entityLoading">
+                <template v-slot:after>
+                  <q-btn round
+                         dense
+                         flat
+                         icon="send"
+                         :loading="entityLoading"
+                         @click="sendReply" />
+                </template>
+              </q-input>
+            </div>
+          </template>
+        </div>
+      </div>
+    </q-card-section>
+  </q-card>
 </template>
 
 <script>
@@ -186,6 +271,9 @@ export default {
   data () {
     return {
       mounted: false,
+      replyText: null,
+      entityLoading: true,
+      graderDescriptionLoading: 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' }
@@ -215,10 +303,73 @@ export default {
     }
   },
   mounted () {
+    this.loadAuthData()
     this.getAnswerBook()
     this.mounted = true
   },
   methods: {
+    scrollToObjectionBox() {
+      this.$nextTick(() => {
+        if (this.$refs.objectionBox && this.$refs.objectionBox.$el) {
+          this.$refs.objectionBox.$el.scrollIntoView({ behavior: 'smooth', block: 'end' })
+        }
+      })
+    },
+    closeTicket () {
+      this.$q.dialog({
+        title: 'توجه',
+        message: 'آیا از بستن تیکت اطمینان دارید؟',
+        cancel: true,
+        persistent: true
+      }).onOk(() => {
+        this.entityLoading = true
+        APIGateway.ticket.close(this.$route.params.id)
+          .then(() => {
+            this.$refs.entityEdit.getData()
+          })
+          .catch(() => {
+            this.entityLoading = false
+          })
+      })
+    },
+    miladiToShamsi (miladi) {
+      return Assist.miladiToShamsi(miladi)
+    },
+    isUserMessage (authenticatedUser, message) {
+      return authenticatedUser.id === message.creator_info.id
+    },
+    removeMessage (replyMessageId) {
+      this.$q.dialog({
+        title: 'توجه',
+        message: 'آیا از حذف پیام اطمینان دارید؟',
+        cancel: true,
+        persistent: true
+      }).onOk(() => {
+        this.entityLoading = true
+        APIGateway.ticket.deleteReplyMessage(replyMessageId)
+          .then(() => {
+            this.getAnswerBook()
+          })
+          .catch(() => {
+            this.entityLoading = false
+          })
+      })
+    },
+    sendReply() {
+      this.entityLoading = true
+      const body = this.replyText.replace(/\n/g, '<br>\n')
+      APIGateway.ticket.reply({
+        id: this.answerBook.ticket,
+        body
+      })
+        .then(() => {
+          this.replyText = null
+          this.getAnswerBook()
+        })
+        .catch(() => {
+          this.getAnswerBook()
+        })
+    },
     sendObjection () {
       this.answerBook.loading = true
       this.$refs.objectionEntityEdit.editEntity(false)
@@ -255,6 +406,17 @@ export default {
           this.answerBook.loading = false
         })
     },
+    submitGraderDescription () {
+      this.graderDescriptionLoading = true
+      APIGateway.answerBook.submitGraderDescription(this.answerBook.id, this.answerBook.grader_description)
+        .then(() => {
+          this.graderDescriptionLoading = false
+          this.getAnswerBook()
+        })
+        .catch(() => {
+          this.graderDescriptionLoading = false
+        })
+    },
     submitScore (index) {
       this.scores[index].loading = true
       APIGateway.answerSheet.submitScore(this.scores[index].answerSheetId, this.scores[index].score)
@@ -268,13 +430,40 @@ export default {
     },
     getAnswerBook () {
       this.answerBook.loading = true
+      this.entityLoading = true
       APIGateway.answerBook.get(this.$route.params.answer_book_id)
         .then((answerBook) => {
           this.loadAnswerBook(answerBook)
           this.answerBook.loading = false
+          this.entityLoading = false
         })
         .catch(() => {
           this.answerBook.loading = false
+          this.entityLoading = false
+        })
+    },
+    confirmScores () {
+      this.answerBook.loading = true
+      APIGateway.answerBook.confirmScores(this.$route.params.answer_book_id)
+        .then(() => {
+          this.getAnswerBook()
+          this.answerBook.loading = false
+        })
+        .catch(() => {
+          this.getAnswerBook()
+          this.answerBook.loading = false
+        })
+    },
+    confirmUngradable () {
+      this.answerBook.loading = true
+      APIGateway.answerBook.confirmUngradable(this.$route.params.answer_book_id)
+        .then(() => {
+          this.getAnswerBook()
+          this.answerBook.loading = false
+        })
+        .catch(() => {
+          this.getAnswerBook()
+          this.answerBook.loading = false
         })
     }
   }

+ 129 - 0
src/components/Widgets/Test/ShowTestGradeForGrader/ShowTestGradeForGrader.vue

@@ -4,6 +4,13 @@
     <q-card v-if="!answerBook.loading && answerBook.id">
       <q-card-section class="flex justify-between">
         <div>{{ answerBook.test_info.title }}</div>
+        <q-btn v-if="!answerBook.loading && answerBook.ticket"
+               outline
+               color="primary"
+               class="btn-need-installment"
+               @click="scrollToObjectionBox">
+          مشاهده اعتراض
+        </q-btn>
       </q-card-section>
       <q-separator />
       <q-banner>
@@ -183,6 +190,60 @@
       کمی صبر کنید...
     </div>
   </div>
+  <q-card v-if="mounted && !answerBook.loading && answerBook.id && answerBook.ticket"
+          ref="objectionBox"
+          class="q-mt-md">
+    <q-card-section>
+      <div class="row justify-center">
+        <div class="full-width">
+          <q-skeleton v-if="entityLoading"
+                      type="rect"
+                      width="100%"
+                      height="200px" />
+          <template v-else>
+            {{ answerBook.ticket_info.body }}
+            <q-chat-message v-for="message in answerBook.ticket_info.replies_info.list"
+                            :key="message.id"
+                            :avatar="message.creator_info?.picture ? message.creator_info.picture : 'assets/images/web/sample-avatar.svg'"
+                            :name="message.creator_info?.firstname + ' ' + message.creator_info?.lastname"
+                            text-html
+                            :text="[message.body]"
+                            :sent="!isUserMessage(this.user, message)">
+              <template v-slot:stamp>
+                <div>
+                  {{ miladiToShamsi(message.creation_time) }}
+                </div>
+                <q-btn icon="clear"
+                       round
+                       color="red"
+                       flat
+                       size="xs"
+                       @click="removeMessage(message.id)" />
+                <q-icon v-if="isUserMessage(this.user, message)"
+                        :name="message.read ? 'done_all' : 'done'" />
+              </template>
+            </q-chat-message>
+            <div>
+              <q-input v-if="false"
+                       v-model="replyText"
+                       bottom-slots
+                       autogrow
+                       :loading="entityLoading">
+                <template v-slot:after>
+                  <q-btn round
+                         dense
+                         flat
+                         icon="send"
+                         :loading="entityLoading"
+                         @click="sendReply" />
+                </template>
+              </q-input>
+            </div>
+          </template>
+        </div>
+      </div>
+    </q-card-section>
+  </q-card>
 </template>
 
 <script>
@@ -199,6 +260,8 @@ export default {
   data () {
     return {
       mounted: false,
+      replyText: null,
+      entityLoading: true,
       graderDescriptionLoading: false,
       currentUserIsGrader: false,
       overallAnswerInput: [
@@ -234,10 +297,73 @@ export default {
     }
   },
   mounted () {
+    this.loadAuthData()
     this.getAnswerBook()
     this.mounted = true
   },
   methods: {
+    scrollToObjectionBox() {
+      this.$nextTick(() => {
+        if (this.$refs.objectionBox && this.$refs.objectionBox.$el) {
+          this.$refs.objectionBox.$el.scrollIntoView({ behavior: 'smooth', block: 'end' })
+        }
+      })
+    },
+    closeTicket () {
+      this.$q.dialog({
+        title: 'توجه',
+        message: 'آیا از بستن تیکت اطمینان دارید؟',
+        cancel: true,
+        persistent: true
+      }).onOk(() => {
+        this.entityLoading = true
+        APIGateway.ticket.close(this.$route.params.id)
+          .then(() => {
+            this.$refs.entityEdit.getData()
+          })
+          .catch(() => {
+            this.entityLoading = false
+          })
+      })
+    },
+    miladiToShamsi (miladi) {
+      return Assist.miladiToShamsi(miladi)
+    },
+    isUserMessage (authenticatedUser, message) {
+      return authenticatedUser.id === message.creator_info.id
+    },
+    removeMessage (replyMessageId) {
+      this.$q.dialog({
+        title: 'توجه',
+        message: 'آیا از حذف پیام اطمینان دارید؟',
+        cancel: true,
+        persistent: true
+      }).onOk(() => {
+        this.entityLoading = true
+        APIGateway.ticket.deleteReplyMessage(replyMessageId)
+          .then(() => {
+            this.getAnswerBook()
+          })
+          .catch(() => {
+            this.entityLoading = false
+          })
+      })
+    },
+    sendReply() {
+      this.entityLoading = true
+      const body = this.replyText.replace(/\n/g, '<br>\n')
+      APIGateway.ticket.reply({
+        id: this.answerBook.ticket,
+        body
+      })
+        .then(() => {
+          this.replyText = null
+          this.getAnswerBook()
+        })
+        .catch(() => {
+          this.getAnswerBook()
+        })
+    },
     sendObjection () {
       this.answerBook.loading = true
       this.$refs.objectionEntityEdit.editEntity(false)
@@ -301,13 +427,16 @@ export default {
     },
     getAnswerBook () {
       this.answerBook.loading = true
+      this.entityLoading = true
       APIGateway.answerBook.get(this.$route.params.answer_book_id)
         .then((answerBook) => {
           this.loadAnswerBook(answerBook)
           this.answerBook.loading = false
+          this.entityLoading = false
         })
         .catch(() => {
           this.answerBook.loading = false
+          this.entityLoading = false
         })
     },
     confirmScores () {

+ 99 - 1
src/components/Widgets/Test/TestShow/TestShow.vue

@@ -4,6 +4,13 @@
     <q-card v-if="!answerBook.loading && answerBook.id">
       <q-card-section class="flex justify-between">
         <div>{{ answerBook.test_info.title }}</div>
+        <q-btn v-if="!answerBook.loading && answerBook.ticket"
+               outline
+               color="primary"
+               class="btn-need-installment"
+               @click="scrollToObjectionBox">
+          مشاهده اعتراض
+        </q-btn>
       </q-card-section>
       <q-separator />
       <q-banner>
@@ -126,6 +133,52 @@
       کمی صبر کنید...
     </div>
   </div>
+  <q-card v-if="!answerBook.loading && answerBook.ticket"
+          ref="objectionBox"
+          class="q-mt-md">
+    <div class="q-mb-md">متن:</div>
+    <div>{{ ticket_info.body }}</div>
+    <q-card-section>
+      <div class="row justify-center">
+        <div class="full-width">
+          <q-skeleton v-if="entityLoading"
+                      type="rect"
+                      width="100%"
+                      height="200px" />
+          <template v-else>
+            <q-chat-message v-for="message in ticket_info.replies_info.list"
+                            :key="message.id"
+                            :avatar="message.creator_info.picture ? message.creator_info.picture : '/assets/images/web/default-avatar.png'"
+                            :name="isUserMessage(authenticatedUser, message) ? message.creator_info.firstname + ' ' + message.creator_info.lastname : 'کارشناس'"
+                            text-html
+                            :text="[message.body]"
+                            :sent="!isUserMessage(authenticatedUser, message)">
+              <template v-slot:stamp>
+                <div>
+                  {{ miladiToShamsi(message.creation_time) }}
+                </div>
+              </template>
+            </q-chat-message>
+            <div v-if="['OBJECTED', 'OBJECTION_RESPONDED'].includes(answerBook.status)">
+              <q-input v-model="replyText"
+                       bottom-slots
+                       autogrow
+                       :loading="entityLoading">
+                <template v-slot:after>
+                  <q-btn round
+                         dense
+                         flat
+                         icon="send"
+                         :loading="entityLoading"
+                         @click="sendReply" />
+                </template>
+              </q-input>
+            </div>
+          </template>
+        </div>
+      </div>
+    </q-card-section>
+  </q-card>
 </template>
 
 <script>
@@ -135,6 +188,8 @@ 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'
+import { Ticket } from 'src/models/Ticket.js'
+import { User } from 'src/models/User.js'
 
 export default {
   name: 'TestShow',
@@ -142,7 +197,11 @@ export default {
   mixins: [mixinWidget],
   data () {
     return {
+      ticket: new Ticket(),
+      replyText: null,
       mounted: false,
+      entityLoading: true,
+      authenticatedUser: new User(),
       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' }
@@ -153,7 +212,8 @@ export default {
       ],
       questionInputs: [],
       objectionInputs: [
-        { type: 'input', name: 'objection_request', responseKey: 'objection_request', label: 'متن اعتراض', placeholder: ' ', inputType: 'textarea', col: 'col-12' }
+        { type: 'input', name: 'objection_request', responseKey: 'objection_request', label: 'متن اعتراض', placeholder: ' ', inputType: 'textarea', col: 'col-12' },
+        { type: 'hidden', name: 'ticket_info', responseKey: 'ticket_info' }
       ],
       objectionApi: APIGateway.answerBook.APIAdresses.submitObjectionRequest(this.$route.params.answer_book_id),
       participateType: null,
@@ -175,14 +235,49 @@ export default {
       const formattedSeconds = seconds.toString().padStart(2, '0')
 
       return formattedHours + ':' + formattedMinutes + ':' + formattedSeconds
+    },
+    ticket_info () {
+      return this.objectionInputs.find(input => input.name === 'ticket_info').value
     }
   },
   mounted () {
     // this.getAnswerBook()
     this.getTestQuestions()
+    this.loadAuthData()
     this.mounted = true
   },
   methods: {
+    scrollToObjectionBox() {
+      this.$nextTick(() => {
+        if (this.$refs.objectionBox && this.$refs.objectionBox.$el) {
+          this.$refs.objectionBox.$el.scrollIntoView({ behavior: 'smooth', block: 'end' })
+        }
+      })
+    },
+    miladiToShamsi (miladi) {
+      return Assist.miladiToShamsi(miladi)
+    },
+    isUserMessage (authenticatedUser, message) {
+      return authenticatedUser.id === message.creator_info.id
+    },
+    loadAuthData () {
+      this.authenticatedUser = this.$store.getters['Auth/user']
+    },
+    sendReply() {
+      this.entityLoading = true
+      const body = this.replyText.replace(/\n/g, '<br>\n')
+      APIGateway.ticket.reply({
+        id: this.answerBook.ticket,
+        body
+      })
+        .then(() => {
+          this.replyText = null
+          this.getTestQuestions()
+        })
+        .catch(() => {
+          this.getTestQuestions()
+        })
+    },
     sendObjection () {
       this.answerBook.loading = true
       this.$refs.objectionEntityEdit.editEntity(false)
@@ -255,14 +350,17 @@ export default {
         .then((answerBook) => {
           this.answerBook = new AnswerBook(answerBook)
           this.answerBook.loading = false
+          this.entityLoading = 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)
+          FormBuilderAssist.setAttributeByName(this.objectionInputs, 'ticket_info', 'value', new Ticket(this.answerBook.ticket_info))
         })
         .catch(() => {
           this.answerBook.loading = false
+          this.entityLoading = false
           this.backToClassList()
         })
     }

+ 102 - 0
src/components/Widgets/TranscriptSheetHistory/AdminTranscriptSheetHistoryCreate/AdminTranscriptSheetHistoryCreate.vue

@@ -0,0 +1,102 @@
+<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'
+
+const BtnControlComp = shallowRef(BtnControl)
+
+export default {
+  name: 'AdminTranscriptSheetHistoryCreate',
+  components: { EntityCreate },
+  mixins: [mixinWidget],
+  data () {
+    const unitId = this.$route.params.unit_id
+    return {
+      mounted: false,
+      entityLoading: false,
+      api: APIGateway.transcriptSheetHistory.APIAdresses.base,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Unit.TranscriptSheetHistory.Show',
+      inputs: [
+        { type: 'input', name: 'national_code', responseKey: 'national_code', label: 'کد ملی', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'input', name: 'score', responseKey: 'score', label: 'نمره', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'hidden', name: 'unit', responseKey: 'unit', value: unitId },
+        { 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.TranscriptSheetHistory.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/TranscriptSheetHistory/AdminTranscriptSheetHistoryCreate/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>

+ 133 - 0
src/components/Widgets/TranscriptSheetHistory/AdminTranscriptSheetHistoryIndex/AdminTranscriptSheetHistoryIndex.vue

@@ -0,0 +1,133 @@
+<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="transcriptSheetHistoryList"
+                  v-model:value="transcriptSheetHistoryListInputs"
+                  :title="'لیست تاریخچه نمرات'"
+                  :api="transcriptSheetHistoryListApi"
+                  :table="transcriptSheetHistoryListTable"
+                  :table-keys="transcriptSheetHistoryListTableKeys"
+                  :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.TranscriptSheetHistory.Test.Create', params: {classroom_id: $route.params.classroom_id, test_set_id:inputData.props.row.id}, query: $route.query}"
+                   :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 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'
+
+export default {
+  name: 'TranscriptSheetHistoryShow',
+  components: {
+    EntityIndex
+  },
+  mixins: [mixinAuthData],
+  data () {
+    return {
+      mounted: false,
+      createInvoiceLoading: false,
+      classroom: new Classroom(),
+
+      dropClassroomByAdminLoading: false,
+      exportReportLoading: false,
+      transcriptSheetHistoryListInputs: [
+        { type: 'hidden', name: 'unit', value: this.$route.params.unit_id }
+      ],
+      transcriptSheetHistoryListApi: APIGateway.transcriptSheetHistory.APIAdresses.base,
+      transcriptSheetHistoryListTable: {
+        columns: [
+          {
+            name: 'id',
+            required: true,
+            label: 'شناسه',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'national_code',
+            required: true,
+            label: 'کد ملی',
+            align: 'left',
+            field: row => row.national_code
+          },
+          {
+            name: 'score',
+            required: true,
+            label: 'نمره',
+            align: 'left',
+            field: row => row.score
+          },
+          {
+            name: 'creation_time',
+            required: true,
+            label: 'تاریخ ایجاد',
+            align: 'left',
+            field: row => ShamsiDate.getDateTime(row.creation_time)
+          },
+          {
+            name: 'last_modification_time',
+            required: true,
+            label: 'تاریخ آخرین تغییر',
+            align: 'left',
+            field: row => ShamsiDate.getDateTime(row.last_modification_time)
+          },
+          {
+            name: 'actions',
+            required: true,
+            label: 'عملیات',
+            align: 'left',
+            field: ''
+          }
+        ]
+      },
+      transcriptSheetHistoryListTableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      }
+    }
+  },
+  mounted () {
+    this.$refs.transcriptSheetHistoryList.search()
+    this.mounted = true
+  },
+  methods: {
+  }
+}
+</script>

+ 21 - 0
src/components/Widgets/TranscriptSheetHistory/AdminTranscriptSheetHistoryIndex/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>

+ 176 - 0
src/components/Widgets/TranscriptSheetHistory/AdminTranscriptSheetHistoryShow/AdminTranscriptSheetHistoryShow.vue

@@ -0,0 +1,176 @@
+<template>
+  <div class="AdminTestShow"
+       :style="localOptions.style">
+    <q-skeleton v-if="!unit.id || !transcriptSheetHistory.id"
+                type="text"
+                width="200px" />
+    <breadcrumbs v-else
+                 style="margin-top: 29px; margin-bottom: 19px;" />
+    <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 { Unit } from 'src/models/Unit.js'
+import { TranscriptSheetHistory } from 'src/models/TranscriptSheetHistory.js'
+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 Breadcrumbs from 'src/components/Widgets/Breadcrumbs/Breadcrumbs.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+
+export default {
+  name: 'AdminTestShow',
+  components: { EntityEdit, Breadcrumbs },
+  mixins: [mixinWidget],
+  data () {
+    const unitId = this.$route.params.unit_id
+    const transcriptSheetHistoryId = this.$route.params.id
+    return {
+      mounted: false,
+      unit: new Unit(),
+      TranscriptSheetHistory: new TranscriptSheetHistory(),
+      entityLoading: false,
+      api: APIGateway.transcriptSheetHistory.APIAdresses.byId(transcriptSheetHistoryId),
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      inputs: [
+        { type: 'input', name: 'national_code', responseKey: 'national_code', label: 'کد ملی', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'input', name: 'score', responseKey: 'score', label: 'نمره', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'hidden', name: 'unit', responseKey: 'unit', value: unitId },
+        { type: 'hidden', name: 'id', responseKey: 'id' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'تایید نهایی', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' }
+      ]
+    }
+  },
+  mounted() {
+    this.setActionBtn()
+    this.mounted = true
+    Promise.all([
+      this.getUnit(),
+      this.getTranscriptSheetHistory()
+    ])
+      .then(() => {
+        this.updateBreadcrumbs()
+      })
+      .catch(() => {
+      })
+  },
+  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
+        })
+    },
+    getUnit () {
+      return new Promise((resolve, reject) => {
+        this.unit.loading = true
+        APIGateway.unit.get(this.$route.params.unit_id)
+          .then((unit) => {
+            this.unit = new Unit(unit)
+            this.unit.loading = false
+            resolve()
+          })
+          .catch(() => {
+            this.unit.loading = false
+            reject()
+          })
+      })
+    },
+    getTranscriptSheetHistory () {
+      return new Promise((resolve, reject) => {
+        this.transcriptSheetHistory.loading = true
+        APIGateway.transcriptSheetHistory.get(this.$route.params.id)
+          .then((transcriptSheetHistory) => {
+            this.transcriptSheetHistory = new TranscriptSheetHistory(transcriptSheetHistory)
+            this.transcriptSheetHistory.loading = false
+            resolve()
+          })
+          .catch(() => {
+            this.transcriptSheetHistory.loading = false
+            reject()
+          })
+      })
+    },
+    updateBreadcrumbs () {
+      this.$store.commit('AppLayout/updateBreadcrumbs', {
+        visible: true,
+        loading: false,
+        path: [
+          {
+            label: 'درس ها و چارت آموزشی',
+            to: { name: 'Admin.Category.Index' }
+          },
+          {
+            label: this.unit.category_info.title,
+            to: { name: 'Admin.Category.Show', params: { id: this.unit.category_info.id } }
+          },
+          {
+            label: this.unit.title,
+            to: { name: 'Admin.Unit.Show', params: { id: this.unit.id } }
+          },
+          {
+            // label: this.testSet.title
+            label: 'تاریخچه نمره'
+          }
+        ]
+      })
+    }
+  }
+}
+</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/TranscriptSheetHistory/AdminTranscriptSheetHistoryShow/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>

+ 10 - 0
src/css/Theme/Controls/input.scss

@@ -1,5 +1,15 @@
 .q-field {
   $input-height: 48px;
+  .q-field__control {
+    height: $input-height;
+    .q-field__marginal {
+      height: $input-height;
+    }
+    .q-field__prepend {
+      padding-left: 12px;
+      padding-right: 0;
+    }
+  }
   &.q-field--outlined {
     .q-field__inner {
       .q-field__control {

+ 25 - 2
src/models/AnswerBook.js

@@ -2,6 +2,7 @@ import { User } from 'src/models/User.js'
 import { Test } from 'src/models/Test.js'
 import { Collection, Model } from 'js-abstract-model'
 import { AnswerSheetList } from 'src/models/AnswerSheet.js'
+import { Ticket } from 'src/models/Ticket'
 
 class AnswerBook extends Model {
   constructor (data) {
@@ -37,6 +38,11 @@ class AnswerBook extends Model {
       { key: 'objection_request' },
       { key: 'objection_result' },
       { key: 'objection_time' },
+      { key: 'ticket' },
+      {
+        key: 'ticket_info',
+        relatedModel: Ticket
+      },
       {
         key: 'answer_sheet_info',
         relatedModel: AnswerSheetList
@@ -83,11 +89,11 @@ class AnswerBook extends Model {
             value: 'CREATED'
           },
           {
-            label: 'ارسال نشده',
+            label: 'در انتظار اتمام آزمون',
             value: 'FETCHED'
           },
           {
-            label: 'ارسال شده',
+            label: 'در انتظار تصحیح',
             value: 'SUBMITTED'
           },
           {
@@ -106,6 +112,10 @@ class AnswerBook extends Model {
             label: 'اعتراض شده',
             value: 'OBJECTED'
           },
+          {
+            label: 'پاسخ اعتراض',
+            value: 'OBJECTION_RESPONDED'
+          },
           {
             label: 'نهایی شده',
             value: 'FINALIZED'
@@ -113,6 +123,19 @@ class AnswerBook extends Model {
         ]
       }
     })
+    // ذخیره مقدار اولیه‌ی status
+    this._originalStatus = this.status
+  }
+
+  get status() {
+    if (this._originalStatus === 'FETCHED' && this.test_info.status !== 'HOLDING') {
+      return 'SUBMITTED'
+    }
+    return this._originalStatus
+  }
+
+  set status(value) {
+    this._originalStatus = value
   }
 }
 

+ 4 - 0
src/models/Media.js

@@ -54,6 +54,10 @@ class Media extends Model {
           {
             label: 'سند',
             value: 'DOCUMENT'
+          },
+          {
+            label: 'سایر',
+            value: 'OTHER'
           }
         ]
       },

+ 2 - 1
src/models/SessionAttendanceSheets.js

@@ -36,7 +36,8 @@ class SessionAttendanceSheets extends Model {
       { key: 'session' },
       { key: 'session_info' },
       { key: 'assignment_status' },
-      { key: 'creation_time' }
+      { key: 'creation_time' },
+      { key: 'attendance_score' }
     ], {
       status: {
         infoKey: 'is_answer_verified_info',

+ 26 - 0
src/models/TranscriptSheetHistory.js

@@ -0,0 +1,26 @@
+import { Unit } from 'src/models/Unit.js'
+import { Model, Collection } from 'js-abstract-model'
+
+class TranscriptSheetHistory extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      { key: 'unit' },
+      {
+        key: 'unit_info',
+        relatedModel: Unit
+      },
+      { key: 'national_code' },
+      { key: 'score' },
+      { key: 'creation_time' },
+      { key: 'last_modification_time' }
+    ])
+  }
+}
+
+class TranscriptSheetHistoryList extends Collection {
+  model () {
+    return TranscriptSheetHistory
+  }
+}
+export { TranscriptSheetHistory, TranscriptSheetHistoryList }

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

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

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

+ 140 - 0
src/pages/Admin/Unit/show.vue

@@ -21,6 +21,8 @@
              label="لیست جلسات" />
       <q-tab name="quiz"
              label="آزمون" />
+      <q-tab name="transcriptSheetHistory"
+             label="تاریخچه کارنامه" />
     </q-tabs>
     <q-separator />
     <q-tab-panels v-if="mounted"
@@ -134,6 +136,57 @@
           </template>
         </entity-index>
       </q-tab-panel>
+      <q-tab-panel name="transcriptSheetHistory"
+                   class="q-pa-none">
+        <entity-index v-if="mounted"
+                      ref="transcriptSheetHistoryEntityIndex"
+                      v-model:value="transcriptSheetHistoryFilterInputs"
+                      title="لیست نمرات"
+                      :api="transcriptSheetHistoryApi"
+                      :table="transcriptSheetHistoryTable"
+                      :table-keys="transcriptSheetHistoryTableKeys"
+                      :show-reload-button="false"
+                      :show-search-button="false"
+                      :show-expand-button="false">
+          <template #toolbar>
+            <div class="flex justify-end">
+              <q-btn color="primary"
+                     outline
+                     label="افزودن نمره"
+                     :loading="newTranscriptSheetHistoryLoading"
+                     :to="{name: 'Admin.Unit.TranscriptSheetHistory.Create', params: {unit_id: $route.params.id}}" />
+            </div>
+          </template>
+          <template #after-index-table>
+            <q-separator class="q-my-lg" />
+            <entity-edit ref="uploadExcelEntity"
+                         v-model:value="uploadInputs"
+                         title="ارسال لیست نمرات"
+                         :loaded-data="{}"
+                         :api="uploadAPI"
+                         :show-reload-button="false"
+                         :show-expand-button="false"
+                         :show-save-button="false"
+                         :show-close-button="false" />
+          </template>
+          <template #entity-index-table-cell="{inputData, showConfirmRemoveDialog}">
+            <template v-if="inputData.col.name === 'actions'">
+              <div class="q-gutter-md">
+                <div class="action-column-entity-index">
+                  <q-btn size="md"
+                         color="primary"
+                         label="تعیین جزییات"
+                         :to="{name: 'Admin.Unit.TranscriptSheetHistory.Show', params: {id: inputData.props.row.id}}" />
+                  <delete-btn @click="showConfirmRemoveDialog(inputData.props.row, 'id', getRemoveMessage(inputData.props.row))" />
+                </div>
+              </div>
+            </template>
+            <template v-else>
+              {{ inputData.col.value }}
+            </template>
+          </template>
+        </entity-index>
+      </q-tab-panel>
     </q-tab-panels>
   </q-card>
 </template>
@@ -147,10 +200,14 @@ import DeleteBtn from 'src/components/Control/DeleteBtn.vue'
 import { UnitCategoryList } from 'src/models/UnitCategory.js'
 import PostRequisites from 'src/components/Control/PostRequisites.vue'
 import FormBuilderInputEditor from 'src/components/FormBuilderCustumComponents/FormBuilderInputEditor.vue'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import BtnControl from 'src/components/Control/btn.vue'
 
 const PostRequisitesComp = shallowRef(PostRequisites)
 const FormBuilderInputEditorComp = shallowRef(FormBuilderInputEditor)
 
+const BtnControlComp = shallowRef(BtnControl)
+
 export default {
   name: 'Admin.Category.Index',
   components: {
@@ -162,6 +219,7 @@ export default {
     return {
       tab: 'UnitInfo',
       mounted: false,
+      newTranscriptSheetHistoryLoading: false,
       newSessionLoading: false,
       newSessionName: '-',
       newSessionSessionCount: null,
@@ -177,6 +235,13 @@ export default {
         { type: 'hidden', name: 'category', responseKey: 'category_info.id' }
       ],
 
+      uploadInputs: [
+        { type: 'hidden', name: 'type', value: 'excel' },
+        { type: 'file', name: 'file', label: 'انتخاب فایل', placeholder: ' ', value: null, col: 'col-md-8 col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ارسال', placeholder: ' ', atClick: () => {}, col: 'col-md-4 col-12' }
+      ],
+      uploadAPI: APIGateway.transcriptSheetHistory.APIAdresses.importList,
+
       categories: new UnitCategoryList(),
       units: new UnitCategoryList(),
       prerequisite: {
@@ -293,11 +358,71 @@ export default {
         currentPage: 'current',
         perPage: 'per_page',
         pageKey: 'page'
+      },
+
+      transcriptSheetHistoryApi: APIGateway.transcriptSheetHistory.APIAdresses.base,
+      transcriptSheetHistoryTable: {
+        columns: [
+          {
+            name: 'id',
+            required: true,
+            label: 'شناسه',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'national_code',
+            required: true,
+            label: 'کد ملی',
+            align: 'left',
+            field: row => row.national_code
+          },
+          {
+            name: 'score',
+            required: true,
+            label: 'نمره',
+            align: 'left',
+            field: row => row.score
+          },
+          {
+            name: 'creation_time',
+            required: true,
+            label: 'تاریخ ایجاد',
+            align: 'left',
+            field: row => ShamsiDate.getDateTime(row.creation_time)
+          },
+          {
+            name: 'last_modification_time',
+            required: true,
+            label: 'تاریخ آخرین تغییر',
+            align: 'left',
+            field: row => ShamsiDate.getDateTime(row.last_modification_time)
+          },
+          {
+            name: 'actions',
+            required: true,
+            label: 'عملیات',
+            align: 'left',
+            field: ''
+          }
+        ],
+        data: []
+      },
+      transcriptSheetHistoryFilterInputs: [
+        { type: 'hidden', name: 'unit', value: this.$route.params.id }
+      ],
+      transcriptSheetHistoryTableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
       }
     }
   },
   mounted () {
     // this.getCategories()
+    this.setActionBtn()
     this.mounted = true
   },
   methods: {
@@ -345,6 +470,21 @@ export default {
         .catch(() => {
           this.newSessionLoading = false
         })
+    },
+    setActionBtn () {
+      FormBuilderAssist.setAttributeByName(this.uploadInputs, 'btn', 'atClick', this.uploadExcelEntity)
+    },
+    search () {
+      this.$refs.entityIndex.search()
+    },
+    uploadExcelEntity () {
+      this.$refs.uploadExcelEntity.editEntity(false)
+        .then(() => {
+          this.search()
+        })
+        .catch(() => {
+          this.search()
+        })
     }
   }
 }

+ 82 - 2
src/pages/User/Profile/classroomInfo.vue

@@ -92,7 +92,12 @@
                       :table-grid-size="$q.screen.lt.sm"
                       :show-expand-button="false"
                       :show-reload-button="false"
-                      :show-search-button="false">
+                      :show-search-button="false"
+                      @on-page-changed="onPageChanged">
+          <template v-if='classroom.holding_type === "OFFLINE"'
+                    #before-form-builder>
+            مجموع نمره  حضور و غیاب کسب شده از ۱۰۰ نمره : {{ totalAttendanceScore }}
+          </template>
           <template #entity-index-table-cell="{inputData}">
             <template v-if="inputData.col.name === 'number'">
               {{ inputData.rowNumber }}
@@ -446,6 +451,7 @@ export default {
     // const userId = this.$store.getters['Auth/user'].id
     return {
       mounted: false,
+      totalAttendanceScore: 0,
       teamInfoDialog: false,
       selectedTeam: null,
       selectedTest: {
@@ -457,7 +463,8 @@ export default {
       tab: 'classroomInfo',
       inputs: [],
       sessionsInputs: [
-        { type: 'hidden', name: 'classroom', value: classroomId }
+        { type: 'hidden', name: 'classroom', value: classroomId },
+        { type: 'hidden', name: 'per_page', value: 100 }
       ],
       api: APIGateway.classroom.get(classroomId),
       sessionsApi: APIGateway.session.APIAdresses.attendanceSheets,
@@ -882,6 +889,79 @@ export default {
     FormBuilderAssist.setAttributeByName(this.testListInputs, 'owner', 'value', this.user.id)
   },
   methods: {
+    onPageChanged (responseData) {
+      this.totalAttendanceScore = responseData.data.results?.map(session => (new SessionAttendanceSheets(this.getCurrentUserAttendanceSheet(session))).attendance_score || 0) // گرفتن مقدار امتیاز حضور
+        .reduce((sum, value) => sum + value, 0) // محاسبه مجموع
+      if (this.classroom.holding_type === 'OFFLINE') {
+        this.sessionsTable = {
+          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: 'title',
+              required: true,
+              label: 'زمان شروع',
+              align: 'left',
+              field: row => row.beginning_time ? ShamsiDate.getDateTime(row.beginning_time) : '-'
+            },
+            {
+              name: 'title',
+              required: true,
+              label: 'زمان پایان',
+              align: 'left',
+              field: row => row.ending_time ? ShamsiDate.getDateTime(row.ending_time) : '-'
+            },
+            {
+              name: 'assignment_status',
+              required: true,
+              label: 'وضعیت تکلیف',
+              align: 'left',
+              field: row => (new SessionAttendanceSheets(this.getCurrentUserAttendanceSheet(row))).assignment_status_info.label
+            },
+            {
+              name: 'attendance_status',
+              required: true,
+              label: 'وضعیت حضور و غیاب',
+              align: 'left',
+              field: row => (new SessionAttendanceSheets(this.getCurrentUserAttendanceSheet(row))).attendance_status_info.label
+            },
+            {
+              name: 'attendance_score',
+              required: true,
+              label: 'نمره حضور و غیاب',
+              align: 'left',
+              field: row => (new SessionAttendanceSheets(this.getCurrentUserAttendanceSheet(row))).attendance_score
+            },
+            {
+              name: 'action',
+              required: true,
+              label: 'جزییات',
+              align: 'left',
+              field: ''
+            }
+          ]
+        }
+      }
+    },
     showParticipateTypeDialog (testId, answerBookId) {
       this.$store.commit('Test/updateAnswerBook', null)
       this.selectedTest.testId = testId

+ 3 - 1
src/pages/User/Profile/projectInfo.vue

@@ -114,7 +114,9 @@ export default {
       attendanceSheetsShowInputs: [
         { type: 'separator', name: 'space', label: 'انجام پروژه', className: 'custom-separator', col: 'col-12' },
         { type: 'inputEditor', name: 'answer_text', responseKey: 'answer_text', label: 'متن پاسخ', placeholder: ' ', col: 'col-12' },
-        { type: 'file', name: 'answer_attachment', responseKey: 'answer_attachment', label: 'فایل ضمیمه', placeholder: ' ', col: 'col-md-4 col-12' }
+        { type: 'file', name: 'answer_attachment', responseKey: 'answer_attachment', label: 'فایل ضمیمه', placeholder: ' ', col: 'col-md-4 col-12' },
+        { type: 'separator', name: 'space', label: 'توضیحات مصحح', className: 'custom-separator', col: 'col-12' },
+        { type: 'inputEditor', name: 'notation', responseKey: 'notation', label: '', placeholder: ' ', col: 'col-12' }
       ]
     }
   },

+ 9 - 0
src/router/routes/Admin.js

@@ -85,6 +85,15 @@ export default {
             { 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') }
           ]
+        },
+        {
+          name: 'Admin.Unit.TranscriptSheetHistory',
+          path: ':unit_id/transcript_sheet_history',
+          component: () => import('src/layouts/bareLayout.vue'),
+          children: [
+            { name: 'Admin.Unit.TranscriptSheetHistory.Show', path: ':id', component: () => import('src/pages/Admin/TranscriptSheetHistory/Show.vue') },
+            { name: 'Admin.Unit.TranscriptSheetHistory.Create', path: 'create', component: () => import('src/pages/Admin/TranscriptSheetHistory/Create.vue') }
+          ]
         }
       ]
     },

+ 9 - 9
yarn.lock

@@ -4957,22 +4957,22 @@ quasar-app-extension-q-page-builder@^0.4.4:
   dependencies:
     quasar-ui-q-page-builder "^0.4.7"
 
-quasar-crud@^2.2.5:
-  version "2.2.5"
-  resolved "https://registry.yarnpkg.com/quasar-crud/-/quasar-crud-2.2.5.tgz#7b5a3faea71fe25508f3f1776b64fdc01e9a9e98"
-  integrity sha512-Qjy6jt6CwANvNCcI3Fab6m6TKPq1uRe3VZkL73+zMFkUARzHvs7aYGD3uO6aMOBk3doV2eNGoRuAHrMJ1C5ggA==
+quasar-crud@^2.2.7:
+  version "2.2.7"
+  resolved "https://registry.yarnpkg.com/quasar-crud/-/quasar-crud-2.2.7.tgz#15137ea7ace2cdf72f12e49661011f428d8c1f90"
+  integrity sha512-thnCEfq5makEoVByLc3W5EhcSzeWuyY7jSfiIAICqHf8DQOcy1SMTCY5ZCdKYUu9OHSewvuqNEgqd7UjJR+9Lw==
   dependencies:
     "@quasar/extras" "^1.16.9"
     axios "^1.6.8"
     quasar "^2.14.2"
-    quasar-form-builder "^3.5.2"
+    quasar-form-builder "^3.5.4"
     shvl "^3.0.0"
     vue "^3.4.21"
 
-quasar-form-builder@^3.5.2:
-  version "3.5.2"
-  resolved "https://registry.yarnpkg.com/quasar-form-builder/-/quasar-form-builder-3.5.2.tgz#9f63fb4cc4b4656651a6b6c210c5f59d84f6ab0e"
-  integrity sha512-bP9v4TtXybxzGnL8GtfyQyt604mnHb2SJsN90PCrifdAZ9bRCLNo1sZwg6sQe9kFc2deICLyp7Y59iPThY1Siw==
+quasar-form-builder@^3.5.4:
+  version "3.5.4"
+  resolved "https://registry.yarnpkg.com/quasar-form-builder/-/quasar-form-builder-3.5.4.tgz#3b8ad03cf81dc9c5d9b10c9e3bcdaef667ddb7dd"
+  integrity sha512-OEqsGgxXEAjxgwcTGSD7nN9AqPWwmePITZPry2cbNXIzenDj1/HNXJSPoaWzIY6vvFS+NCICcTkv9JAvRHdg2A==
   dependencies:
     "@quasar/extras" "^1.16.9"
     core-js "^3.34.0"