Przeglądaj źródła

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

 Conflicts:
	yarn.lock
HgH 2 lat temu
rodzic
commit
831f99afb2
100 zmienionych plików z 9402 dodań i 86 usunięć
  1. 12 0
      src/api/APIGateway.js
  2. 189 0
      src/api/models/AnswerBook.js
  3. 107 0
      src/api/models/AnswerSheet.js
  4. 20 0
      src/api/models/Classroom.js
  5. 71 0
      src/api/models/Question.js
  6. 57 0
      src/api/models/QuestionChoice.js
  7. 1 0
      src/api/models/Registration.js
  8. 17 1
      src/api/models/SessionTemplate.js
  9. 110 0
      src/api/models/Test.js
  10. 72 0
      src/api/models/TestSet.js
  11. 14 0
      src/api/models/Unit.js
  12. 4 1
      src/assets/ShamsiDate.js
  13. 5 0
      src/components/Control/btn.vue
  14. 1 0
      src/components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue
  15. 6 5
      src/components/FormBuilderCustumComponents/FormBuilderInputEditor.vue
  16. 51 0
      src/components/FormBuilderCustumComponents/FormBuilderTextarea.vue
  17. 107 0
      src/components/FormBuilderCustumComponents/QuestionChoices/ChoicesItem.vue
  18. 219 0
      src/components/FormBuilderCustumComponents/QuestionChoices/QuestionChoices.vue
  19. 134 0
      src/components/FormBuilderCustumComponents/QuestionsCreateAndAppend/CreateQuestionForm.vue
  20. 120 0
      src/components/FormBuilderCustumComponents/QuestionsCreateAndAppend/EditQuestionForm.vue
  21. 505 0
      src/components/FormBuilderCustumComponents/QuestionsCreateAndAppend/QuestionsCreateAndAppend.vue
  22. 428 0
      src/components/FormBuilderCustumComponents/QuestionsSelector/QuestionsSelector.vue
  23. 254 0
      src/components/FormBuilderCustumComponents/UsersOfTestSelector/UsersOfTestSelector.vue
  24. 6 0
      src/components/Template/SideBard/AdminPanelSideBar.vue
  25. 8 0
      src/components/Template/SideBard/UserPanelSideBar.vue
  26. 433 0
      src/components/Widgets/Classroom/AdminClassroomAnswerBookList/AdminClassroomAnswerBookList.vue
  27. 28 0
      src/components/Widgets/Classroom/AdminClassroomAnswerBookList/OptionPanel.vue
  28. 17 11
      src/components/Widgets/Classroom/AdminClassroomShow/AdminClassroomShow.vue
  29. 16 8
      src/components/Widgets/Classroom/AdminClassroomShow/components/GraderList.vue
  30. 8 11
      src/components/Widgets/Classroom/AdminClassroomShow/components/MembersList.vue
  31. 213 0
      src/components/Widgets/Classroom/AdminClassroomShow/components/TestList.vue
  32. 282 0
      src/components/Widgets/Classroom/ClassroomAnswerBookList/ClassroomAnswerBookList.vue
  33. 28 0
      src/components/Widgets/Classroom/ClassroomAnswerBookList/OptionPanel.vue
  34. 235 0
      src/components/Widgets/Classroom/ClassroomAnswerBookSubmitGrade/ClassroomAnswerBookSubmitGrade.vue
  35. 28 0
      src/components/Widgets/Classroom/ClassroomAnswerBookSubmitGrade/OptionPanel.vue
  36. 1 1
      src/components/Widgets/Classroom/ClassroomShow/ClassroomShow.vue
  37. 221 0
      src/components/Widgets/Classroom/MyAsGraderClassroomList/MyAsGraderClassroomList.vue
  38. 28 0
      src/components/Widgets/Classroom/MyAsGraderClassroomList/OptionPanel.vue
  39. 115 46
      src/components/Widgets/Other/Shop/ShopCompleteInfo/ShopCompleteInfo.vue
  40. 258 0
      src/components/Widgets/Other/Shop/ShopCompleteInfo/components/AllQuestions.vue
  41. 118 0
      src/components/Widgets/Other/Shop/ShopCompleteInfo/components/ShowEventTest.vue
  42. 1 1
      src/components/Widgets/Other/ShowClassroomInfo/ShowClassroomInfo.vue
  43. 121 0
      src/components/Widgets/SessionTemplate/AdminEventQuestionCreate/AdminEventQuestionCreate.vue
  44. 21 0
      src/components/Widgets/SessionTemplate/AdminEventQuestionCreate/OptionPanel.vue
  45. 160 0
      src/components/Widgets/SessionTemplate/AdminEventQuestionList/AdminEventQuestionList.vue
  46. 21 0
      src/components/Widgets/SessionTemplate/AdminEventQuestionList/OptionPanel.vue
  47. 125 0
      src/components/Widgets/SessionTemplate/AdminEventQuestionShow/AdminEventQuestionShow.vue
  48. 21 0
      src/components/Widgets/SessionTemplate/AdminEventQuestionShow/OptionPanel.vue
  49. 121 0
      src/components/Widgets/SessionTemplate/AdminSessionTemplateQuestionCreate/AdminSessionTemplateQuestionCreate.vue
  50. 21 0
      src/components/Widgets/SessionTemplate/AdminSessionTemplateQuestionCreate/OptionPanel.vue
  51. 213 0
      src/components/Widgets/SessionTemplate/AdminSessionTemplateQuestionList/AdminSessionTemplateQuestionList.vue
  52. 21 0
      src/components/Widgets/SessionTemplate/AdminSessionTemplateQuestionList/OptionPanel.vue
  53. 120 0
      src/components/Widgets/SessionTemplate/AdminSessionTemplateQuestionShow/AdminSessionTemplateQuestionShow.vue
  54. 21 0
      src/components/Widgets/SessionTemplate/AdminSessionTemplateQuestionShow/OptionPanel.vue
  55. 313 0
      src/components/Widgets/Test/AdminAnswerBookShow/AdminAnswerBookShow.vue
  56. 21 0
      src/components/Widgets/Test/AdminAnswerBookShow/OptionPanel.vue
  57. 149 0
      src/components/Widgets/Test/AdminTestCreate/AdminTestCreate.vue
  58. 21 0
      src/components/Widgets/Test/AdminTestCreate/OptionPanel.vue
  59. 206 0
      src/components/Widgets/Test/AdminTestShow/AdminTestShow.vue
  60. 21 0
      src/components/Widgets/Test/AdminTestShow/OptionPanel.vue
  61. 21 0
      src/components/Widgets/Test/ShowTestGradeForGrader/OptionPanel.vue
  62. 370 0
      src/components/Widgets/Test/ShowTestGradeForGrader/ShowTestGradeForGrader.vue
  63. 21 0
      src/components/Widgets/Test/TestConfirmation/OptionPanel.vue
  64. 97 0
      src/components/Widgets/Test/TestConfirmation/TestConfirmation.vue
  65. 21 0
      src/components/Widgets/Test/TestParticipation/OptionPanel.vue
  66. 285 0
      src/components/Widgets/Test/TestParticipation/TestParticipation.vue
  67. 150 0
      src/components/Widgets/Test/TestParticipation/components/AllQuestions.vue
  68. 85 0
      src/components/Widgets/Test/TestParticipation/components/CompletionPage.vue
  69. 213 0
      src/components/Widgets/Test/TestParticipation/components/SingleQuestion.vue
  70. 21 0
      src/components/Widgets/Test/TestShow/OptionPanel.vue
  71. 303 0
      src/components/Widgets/Test/TestShow/TestShow.vue
  72. 109 0
      src/components/Widgets/TestSet/AdminEventTestSetCreate/AdminEventTestSetCreate.vue
  73. 21 0
      src/components/Widgets/TestSet/AdminEventTestSetCreate/OptionPanel.vue
  74. 168 0
      src/components/Widgets/TestSet/AdminEventTestSetIndex/AdminEventTestSetIndex.vue
  75. 21 0
      src/components/Widgets/TestSet/AdminEventTestSetIndex/OptionPanel.vue
  76. 113 0
      src/components/Widgets/TestSet/AdminEventTestSetShow/AdminEventTestSetShow.vue
  77. 21 0
      src/components/Widgets/TestSet/AdminEventTestSetShow/OptionPanel.vue
  78. 105 0
      src/components/Widgets/TestSet/AdminTestSetCreate/AdminTestSetCreate.vue
  79. 21 0
      src/components/Widgets/TestSet/AdminTestSetCreate/OptionPanel.vue
  80. 158 0
      src/components/Widgets/TestSet/AdminTestSetIndex/AdminTestSetIndex.vue
  81. 21 0
      src/components/Widgets/TestSet/AdminTestSetIndex/OptionPanel.vue
  82. 178 0
      src/components/Widgets/TestSet/AdminTestSetShow/AdminTestSetShow.vue
  83. 21 0
      src/components/Widgets/TestSet/AdminTestSetShow/OptionPanel.vue
  84. 0 1
      src/components/other/VerifyMobileNumber.vue
  85. 125 0
      src/models/AnswerBook.js
  86. 27 0
      src/models/AnswerSheet.js
  87. 4 0
      src/models/Classroom.js
  88. 4 0
      src/models/Event.js
  89. 86 0
      src/models/Question.js
  90. 19 0
      src/models/QuestionChoice.js
  91. 4 0
      src/models/Registration.js
  92. 109 0
      src/models/Test.js
  93. 52 0
      src/models/TestSet.js
  94. 25 0
      src/models/TestSetQuestion.js
  95. 23 0
      src/pages/Admin/Classroom/ClassroomAnswerBookShowGrade.vue
  96. 23 0
      src/pages/Admin/Classroom/ClassroomAnswerBookShowGradeForGrader.vue
  97. 23 0
      src/pages/Admin/Classroom/ClassroomAnswerBookSubmitGrade.vue
  98. 23 0
      src/pages/Admin/Classroom/ClassroomAnswerBooks.vue
  99. 23 0
      src/pages/Admin/Classroom/ClassroomAnswerBooksForGrader.vue
  100. 23 0
      src/pages/Admin/Classroom/MyAsGraderClassrooms.vue

+ 12 - 0
src/api/APIGateway.js

@@ -4,17 +4,22 @@ import UserAPI from 'src/api/models/User.js'
 import PostAPI from 'src/api/models/Post.js'
 import UnitAPI from 'src/api/models/Unit.js'
 import TeamAPI from 'src/api/models/Team.js'
+import TestAPI from 'src/api/models/Test.js'
 import EventAPI from 'src/api/models/Event.js'
 import MediaAPI from 'src/api/models/Media.js'
 import TicketAPI from 'src/api/models/Ticket.js'
 import WalletAPI from 'src/api/models/Wallet.js'
+import TestSetAPI from 'src/api/models/TestSet.js'
 import InvoiceAPI from 'src/api/models/Invoice.js'
 import PaymentAPI from 'src/api/models/Payment.js'
 import SessionAPI from 'src/api/models/Session.js'
 import ContentAPI from 'src/api/models/Content.js'
 import ProjectAPI from 'src/api/models/Project.js'
+import QuestionApi from 'src/api/models/Question.js'
 import ClassroomAPI from 'src/api/models/Classroom.js'
+import AnswerBookApi from 'src/api/models/AnswerBook.js'
 import InstalmentAPI from 'src/api/models/Instalment.js'
+import AnswerSheetApi from 'src/api/models/AnswerSheet.js'
 import TransactionAPI from 'src/api/models/Transaction.js'
 import PageSettingAPI from 'src/api/models/PageSetting.js'
 import PostCategoryAPI from 'src/api/models/PostCategory.js'
@@ -22,6 +27,7 @@ import MediaHashtagAPI from 'src/api/models/MediaHashtag.js'
 import RegistrationAPI from 'src/api/models/Registration.js'
 import UnitCategoryAPI from 'src/api/models/UnitCategory.js'
 import MediaCategoryAPI from 'src/api/models/MediaCategory.js'
+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 ContentCategoryAPI from 'src/api/models/ContentCategory.js'
@@ -42,17 +48,22 @@ export const APIGateway = {
   unit: new UnitAPI(),
   post: new PostAPI(),
   team: new TeamAPI(),
+  test: new TestAPI(),
   event: new EventAPI(),
   media: new MediaAPI(),
   ticket: new TicketAPI(),
   wallet: new WalletAPI(),
+  testSet: new TestSetAPI(),
   project: new ProjectAPI(),
   invoice: new InvoiceAPI(),
   payment: new PaymentAPI(),
   session: new SessionAPI(),
   content: new ContentAPI(),
+  question: new QuestionApi(),
   classroom: new ClassroomAPI(),
+  answerBook: new AnswerBookApi(),
   instalment: new InstalmentAPI(),
+  answerSheet: new AnswerSheetApi(),
   transaction: new TransactionAPI(),
   pageSetting: new PageSettingAPI(),
   registration: new RegistrationAPI(),
@@ -61,6 +72,7 @@ export const APIGateway = {
   mediaHashtag: new MediaHashtagAPI(),
   mediaCategory: new MediaCategoryAPI(),
   ticketCategory: new TicketCategoryAPI(),
+  questionChoice: new QuestionChoiceApi(),
   sessionTemplate: new SessionTemplateAPI(),
   contentCategory: new ContentCategoryAPI(),
   instalmentOffer: new InstalmentOfferAPI(),

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

@@ -0,0 +1,189 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { AnswerBook, AnswerBookList } from 'src/models/AnswerBook.js'
+
+export default class AnswerBookAPI extends APIRepository {
+  constructor() {
+    super('answer_books', appApi)
+    this.APIAdresses = {
+      base: '/lma/answer_books',
+      getOrCreate: '/lma/answer_books/get_or_create',
+      byId: (id) => '/lma/answer_books/' + id,
+      submitGraderDescription: (id) => '/lma/answer_books/' + id + '/submit_grader_description',
+      confirmAnswers: (id) => '/lma/answer_books/' + id + '/confirm_answers',
+      confirmScores: (id) => '/lma/answer_books/' + id + '/confirm_scores',
+      confirmUngradable: (id) => '/lma/answer_books/' + id + '/confirm_ungradable',
+      setGraderToMe: (id) => '/lma/answer_books/' + id + '/set_grader_to_me',
+      submitObjectionResult: (id) => '/lma/answer_books/' + id + '/submit_objection_result',
+      submitObjectionRequest: (id) => '/lma/answer_books/' + id + '/submit_objection_request',
+      getTestQuestions: (id) => '/lma/answer_books/' + id + '/get_test_questions',
+      submitOverallAnswer: (id) => '/lma/answer_books/' + id + '/submit_overall_answer'
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base,
+      byId: (id) => this.name + this.APIAdresses.byId(id),
+      getTestQuestions: (id) => this.name + this.APIAdresses.getTestQuestions(id)
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new AnswerBook(response.data) },
+      post: (response) => { return new AnswerBook(response.data) },
+      put: (response) => { return new AnswerBook(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        test__classroom: null, // Number
+        owner: 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 AnswerBookList(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 AnswerBook(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  getOrCreate (userId, testId) {
+    return this.sendRequest({
+      apiMethod: 'post',
+      api: this.api,
+      request: this.APIAdresses.getOrCreate,
+      data: {
+        owner: userId,
+        test: testId
+      },
+      resolveCallback: (response) => {
+        return new AnswerBook(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  getTestQuestions (id) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.getTestQuestions(id),
+      resolveCallback: (response) => {
+        return new AnswerBook(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  submitGraderDescription (id, graderDescription) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.submitGraderDescription(id),
+      data: {
+        grader_description: graderDescription
+      },
+      resolveCallback: (response) => {
+        return new AnswerBook(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  confirmAnswers (id) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.confirmAnswers(id),
+      resolveCallback: (response) => {
+        return new AnswerBook(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  confirmScores (id) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.confirmScores(id),
+      resolveCallback: (response) => {
+        return new AnswerBook(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  confirmUngradable (id) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.confirmUngradable(id),
+      resolveCallback: (response) => {
+        return new AnswerBook(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  setGraderToMe (answerBookId) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.setGraderToMe(answerBookId),
+      resolveCallback: (response) => {
+        return new AnswerBook(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

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

@@ -0,0 +1,107 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { AnswerSheet, AnswerSheetList } from 'src/models/AnswerSheet.js'
+
+export default class AnswerSheetAPI extends APIRepository {
+  constructor() {
+    super('answer_sheets', appApi)
+    this.APIAdresses = {
+      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'
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base,
+      byId: (id) => this.name + this.APIAdresses.byId(id),
+      getTestQuestions: (id) => this.name + this.APIAdresses.getTestQuestions(id)
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new AnswerSheet(response.data) },
+      post: (response) => { return new AnswerSheet(response.data) },
+      put: (response) => { return new AnswerSheet(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        test__classroom: null, // Number
+        owner: 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 AnswerSheetList(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 AnswerSheet(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  getTestQuestions (id) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.getTestQuestions(id),
+      resolveCallback: (response) => {
+        return new AnswerSheet(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  submitScore (answerSheetsId, score) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.byId(answerSheetsId),
+      data: {
+        score
+      },
+      resolveCallback: (response) => {
+        return new AnswerSheet(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

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

@@ -9,8 +9,10 @@ export default class ClassroomAPI extends APIRepository {
     super('classrooms', appApi, '/lma/classrooms', Classroom)
     this.APIAdresses = {
       base: '/lma/classrooms',
+      myAsGraderClassrooms: '/lma/classrooms/my_as_a_grader_classrooms',
       byId: (id) => '/lma/classrooms/' + id,
       enroll: (id) => '/lma/classrooms/' + id + '/enroll',
+      graders: (id) => '/lma/classrooms/' + id + '/graders',
       drop: (id) => '/lma/classrooms/' + id + '/drop',
       register: (id) => '/lma/classrooms/' + id + '/register',
       leaders: (id) => '/lma/classrooms/' + id + '/leaders',
@@ -175,6 +177,24 @@ export default class ClassroomAPI extends APIRepository {
     })
   }
 
+  appendGraders (data) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.graders(data.classroomId),
+      data: {
+        classroomId: data.classroomId,
+        graders: data.graders
+      },
+      resolveCallback: (response) => {
+        return new ClassroomList(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
   createInvoiceByAdmin (classroomId, userId) {
     return this.sendRequest({
       apiMethod: 'put',

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

@@ -0,0 +1,71 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { Question, QuestionList } from 'src/models/Question.js'
+
+export default class QuestionAPI extends APIRepository {
+  constructor() {
+    super('questions', appApi, '/lma/questions', Question, '/lma/questions')
+    this.APIAdresses = {
+      base: '/lma/questions',
+      byId: (id) => '/lma/questions/' + id
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new Question(response.data) },
+      post: (response) => { return new Question(response.data) },
+      put: (response) => { return new Question(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        category: null, // Number
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new QuestionList(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 Question(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 57 - 0
src/api/models/QuestionChoice.js

@@ -0,0 +1,57 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { QuestionChoice, QuestionChoiceList } from 'src/models/QuestionChoice.js'
+
+export default class QuestionChoiceAPI extends APIRepository {
+  constructor() {
+    super('question_choices', appApi, '/lma/question_choices', QuestionChoice)
+    this.APIAdresses = {
+      base: '/lma/question_choices',
+      byId: (id) => '/lma/question_choices/' + id
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new QuestionChoice(response.data) },
+      post: (response) => { return new QuestionChoice(response.data) },
+      put: (response) => { return new QuestionChoice(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        category: null, // Number
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new QuestionChoiceList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

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

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

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

@@ -7,7 +7,8 @@ export default class SessionTemplateAPI extends APIRepository {
     super('SessionTemplate', appApi, '/lma/categories', SessionTemplate)
     this.APIAdresses = {
       base: '/lma/session_templates',
-      byId: (id) => '/lma/session_templates/' + id
+      byId: (id) => '/lma/session_templates/' + id,
+      appendQuestion: (id) => '/lma/session_templates/' + id + '/append_question'
     }
     this.CacheList = {
       base: this.name + this.APIAdresses.base,
@@ -21,6 +22,7 @@ export default class SessionTemplateAPI extends APIRepository {
       api: this.api,
       request: this.APIAdresses.base,
       data: this.getNormalizedSendData({
+        unit: null, // Number
         category: null, // Number
         per_page: 10, // Number
         page: 1 // Number
@@ -48,6 +50,20 @@ export default class SessionTemplateAPI extends APIRepository {
     })
   }
 
+  get (id) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.byId(id),
+      resolveCallback: (response) => {
+        return new SessionTemplate(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
   create (data) {
     return this.sendRequest({
       apiMethod: 'post',

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

@@ -0,0 +1,110 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { Test, TestList } from 'src/models/Test.js'
+
+export default class TestAPI extends APIRepository {
+  constructor() {
+    super('tests', appApi)
+    this.APIAdresses = {
+      base: '/lma/tests',
+      byId: (id) => '/lma/tests/' + id,
+      testQuestionById: (id) => '/lma/test_questions/' + id,
+      appendQuestion: (id) => '/lma/tests/' + id + '/append_question'
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new Test(response.data) },
+      post: (response) => { return new Test(response.data) },
+      put: (response) => { return new Test(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        event: null, // Number
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new TestList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  get(id) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.byId(id),
+      resolveCallback: (response) => {
+        return new Test(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  createTestForEvent (testTitle, eventId, tstSetId) {
+    return this.sendRequest({
+      apiMethod: 'post',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: {
+        title: testTitle,
+        event: eventId,
+        test_set: tstSetId
+      },
+      resolveCallback: (response) => {
+        return new Test(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  editTestQuestion (data) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.testQuestionById(data.id),
+      data: {
+        mark: data.mark,
+        is_extra_mark: data.is_extra_mark
+      },
+      resolveCallback: () => {
+        return null
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 72 - 0
src/api/models/TestSet.js

@@ -0,0 +1,72 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { TestSet, TestSetList } from 'src/models/TestSet.js'
+
+export default class TestSetAPI extends APIRepository {
+  constructor() {
+    super('test_sets', appApi)
+    this.APIAdresses = {
+      base: '/lma/test_sets',
+      byId: (id) => '/lma/test_sets/' + id,
+      appendQuestion: (id) => '/lma/test_sets/' + id + '/append_question'
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new TestSet(response.data) },
+      post: (response) => { return new TestSet(response.data) },
+      put: (response) => { return new TestSet(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        category: null, // Number
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new TestSetList(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 TestSet(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 14 - 0
src/api/models/Unit.js

@@ -49,6 +49,20 @@ export default class UnitAPI extends APIRepository {
     })
   }
 
+  get (id) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.byId(id),
+      resolveCallback: (response) => {
+        return new Unit(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
   create (data) {
     return this.sendRequest({
       apiMethod: 'post',

+ 4 - 1
src/assets/ShamsiDate.js

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

+ 5 - 0
src/components/Control/btn.vue

@@ -4,6 +4,7 @@
          class="outside-label" />
     <q-btn :label="label"
            :color="color"
+           :outline="outline"
            :class="btnClass"
            :disable="disable"
            :loading="loading"
@@ -27,6 +28,10 @@ export default {
       default: 'primary',
       type: String
     },
+    outline: {
+      default: false,
+      type: Boolean
+    },
     label: {
       default: '',
       type: String

+ 1 - 0
src/components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue

@@ -213,5 +213,6 @@ export default {
 
 <style lang="scss" scoped>
 .ContentMedias {
+  width: 100%;
 }
 </style>

+ 6 - 5
src/components/FormBuilderCustumComponents/FormBuilderInputEditor.vue

@@ -95,7 +95,7 @@
           <q-list dense>
             <q-item tag="label"
                     clickable
-                    @click="color('backColor', highlight)">
+                    @click="setColor('backColor', highlight)">
               <q-item-section side>
                 <q-icon name="highlight" />
               </q-item-section>
@@ -113,7 +113,7 @@
             </q-item>
             <q-item tag="label"
                     clickable
-                    @click="color('foreColor', foreColor)">
+                    @click="setColor('foreColor', foreColor)">
               <q-item-section side>
                 <q-icon name="format_paint" />
               </q-item-section>
@@ -155,19 +155,20 @@ export default {
   },
   data () {
     return {
+      inputData: '',
       foreColor: '#000000',
       highlight: '#ffff00aa'
     }
   },
   watch: {
     value(newValue) {
-      if (newValue === false) {
+      if (!newValue) {
         this.inputData = ''
       }
     }
   },
   created() {
-    if (this.inputData === false) {
+    if (!this.inputData) {
       this.inputData = ''
     }
   },
@@ -175,7 +176,7 @@ export default {
     onPaste () {
       this.inputData = this.inputData.replaceAll(/<!--\[if .*endif\]-->/gms, 'ferret')
     },
-    color (cmd, name) {
+    setColor (cmd, name) {
       const edit = this.$refs.editor
       this.$refs.token.hide()
       edit.caret.restore()

+ 51 - 0
src/components/FormBuilderCustumComponents/FormBuilderTextarea.vue

@@ -0,0 +1,51 @@
+<template>
+  <div class="form-builder-input"
+       :class="customClass">
+    <div class="outsideLabel">{{ placeholder ? label : null }}</div>
+    <q-input v-model="inputData"
+             :name="name"
+             :filled="filled"
+             :rules="rules"
+             :lazy-rules="lazyRules"
+             :label="placeholder ? null : label"
+             :stack-label="!!placeholder"
+             :type="inputType"
+             :disable="disable"
+             :clearable="clearable"
+             :loading="loading"
+             :readonly="readonly"
+             :rounded="rounded"
+             :outlined="outlined"
+             :placeholder="placeholder"
+             :class="customClass"
+             :input-class="customClass"
+             :autogrow="autogrow"
+             @update:model-value="change($event)"
+             @click="onClick"
+             @keypress="onKeyPress" />
+  </div>
+</template>
+
+<script>
+import inputMixin from '../mixins/inputMixin.js'
+export default {
+  name: 'FormBuilderInput',
+  mixins: [inputMixin],
+  props: {
+    name: {
+      default: '',
+      type: String
+    },
+    autogrow: {
+      type: Boolean,
+      default: false
+    },
+    value: {
+      default: '',
+      type: [String, Number, Boolean]
+    }
+  }
+}
+</script>
+
+<style scoped></style>

+ 107 - 0
src/components/FormBuilderCustumComponents/QuestionChoices/ChoicesItem.vue

@@ -0,0 +1,107 @@
+<template>
+  <div class="ChoicesItem">
+    <template v-if="canEdit || canDelete">
+      <div class="title">
+        <q-input v-model="choiceText"
+                 class="full-width"
+                 type="textarea"
+                 @update:model-value="onChangeChoiceText" />
+      </div>
+      <div class="actions">
+        <q-btn v-if="canEdit"
+               icon="edit"
+               outline
+               class="viewBtn"
+               color="primary"
+               @click="onEdit" />
+        <q-btn v-if="canDelete"
+               class="deleteBtn"
+               icon="delete"
+               outline
+               color="red"
+               @click="onDelete" />
+      </div>
+    </template>
+    <div v-else
+         v-html="choiceText" />
+  </div>
+</template>
+
+<script>
+import { QuestionChoice } from 'src/models/QuestionChoice.js'
+
+export default {
+  name: 'ChoicesItem',
+  props: {
+    choice: {
+      type: QuestionChoice,
+      default: new QuestionChoice()
+    },
+    canEdit: {
+      type: Boolean,
+      default: true
+    },
+    canDelete: {
+      type: Boolean,
+      default: true
+    }
+  },
+  emits: ['update:choice', 'onEdit', 'onDelete'],
+  data () {
+    return {
+      choiceText: null,
+      entityCreateLoading: false
+    }
+  },
+  mounted() {
+    if (this.canEdit || this.canDelete) {
+      this.choiceText = this.choice.text.replace(/<br>/g, '\n')
+    } else {
+      this.choiceText = this.choice.text.replace(/\n/g, '<br>')
+    }
+  },
+  methods: {
+    onChangeChoiceText () {
+      const oldChoice = this.choice
+      oldChoice.text = this.choiceText.replace(/\n/g, '<br>')
+      this.$emit('update:choice', oldChoice)
+    },
+    onEdit () {
+      this.$emit('onEdit')
+    },
+    onDelete () {
+      this.$emit('onDelete')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.ChoicesItem {
+  background: #F9F9F9;
+  border: 1px solid #DFE1EC;
+  border-radius: 10px;
+  padding: 10px 16px 10px 21px;
+  display: flex;
+  flex-flow: row;
+  justify-content: space-between;
+  align-items: center;
+  .title {
+    width: calc( 100% - 120px );
+  }
+  .actions {
+    display: flex;
+    flex-flow: row;
+    justify-content: flex-end;
+    .q-btn {
+      width: 42px;
+      height: 42px;
+      &.viewBtn {
+      }
+      &.deleteBtn {
+        margin-left: 16px;
+      }
+    }
+  }
+}
+</style>

+ 219 - 0
src/components/FormBuilderCustumComponents/QuestionChoices/QuestionChoices.vue

@@ -0,0 +1,219 @@
+<template>
+  <div class="QuestionChoices">
+    <q-linear-progress v-if="fetchLoading"
+                       indeterminate />
+    <div v-if="canAddChoice && !readonly"
+         class="q-mb-md">
+      <entity-create ref="entityCreate"
+                     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" />
+      <q-btn color="primary"
+             class="full-width q-mt-md"
+             :loading="entityCreateLoading"
+             @click="entityCreate">
+        افزودن گزینه
+      </q-btn>
+    </div>
+    <div v-for="(choice, choiceIndex) in choices.list"
+         :key="choice.id"
+         class="choice-item">
+      <div class="choice-item-radio">
+        <q-radio v-model="localCorrectChoiceIndex"
+                 :val="choiceIndex"
+                 @update:model-value="onChangeCorrectChoiceIndex" />
+      </div>
+      <div class="choice-item-component">
+        <choices-item :choice="choice"
+                      :can-edit="canEdit"
+                      :can-delete="canDelete"
+                      @onEdit="onEdit(choice)"
+                      @onDelete="onDelete(choice)" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { EntityCreate } from 'quasar-crud'
+import ChoicesItem from './ChoicesItem.vue'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import { QuestionChoice, QuestionChoiceList } from 'src/models/QuestionChoice.js'
+
+export default {
+  name: 'QuestionChoices',
+  components: { ChoicesItem, EntityCreate },
+  props: {
+    value: {
+      default: () => [],
+      type: [Array, String, Number, Boolean]
+    },
+    choicesInfo: {
+      default: () => [],
+      type: [Array, String, Number, Boolean]
+    },
+    correctChoiceIndex: {
+      default: 0,
+      type: Number
+    },
+    disable: {
+      default: false,
+      type: Boolean
+    },
+    participateMode: {
+      type: Boolean,
+      default: false
+    },
+    canEdit: {
+      type: Boolean,
+      default: true
+    },
+    canDelete: {
+      type: Boolean,
+      default: true
+    },
+    canAddChoice: {
+      type: Boolean,
+      default: true
+    },
+    sourceType: {
+      type: String,
+      default: 'CONTENT'
+    },
+    readonly: {
+      default: false,
+      type: Boolean
+    },
+    showPageLink: {
+      default: true,
+      type: Boolean
+    },
+    showDownloadLink: {
+      default: false,
+      type: Boolean
+    }
+  },
+  emits: ['update:value'],
+  data () {
+    return {
+      fetchLoading: false,
+      localCorrectChoiceIndex: 0,
+      choices: new QuestionChoiceList(),
+      api: APIGateway.questionChoice.APIAdresses.base,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      inputs: [
+        { type: 'input', name: 'text', responseKey: 'text', label: 'متن', inputType: 'textarea', placeholder: ' ', col: 'col-12' }
+      ],
+      entityCreateLoading: false
+    }
+  },
+  watch: {
+    value: {
+      handler () {
+        if (this.participateMode) {
+          this.localCorrectChoiceIndex = this.value
+          return
+        }
+
+        this.inputData = this.value
+        if (this.value.length > 0 && this.value[0].id) {
+          this.choices = new QuestionChoiceList(this.value)
+          this.emitValue(this.choices.list.map(item => item.id))
+        }
+      },
+      immediate: true
+    },
+    correctChoiceIndex () {
+      this.localCorrectChoiceIndex = this.correctChoiceIndex
+    }
+  },
+  created () {
+    this.choices = new QuestionChoiceList(this.choicesInfo)
+    FormBuilderAssist.setAttributeByName(this.inputs, 'source_type', 'value', this.sourceType)
+    this.inputData = this.value
+    if (this.participateMode) {
+      this.localCorrectChoiceIndex = this.value || 0
+    } else {
+      this.localCorrectChoiceIndex = this.correctChoiceIndex
+    }
+  },
+  methods: {
+    emitValue (value) {
+      this.$emit('update:value', value)
+    },
+    onChangeCorrectChoiceIndex () {
+      // this.$emit('update:correctChoiceIndex', this.localCorrectChoiceIndex)
+      if (this.participateMode) {
+        this.emitValue(this.localCorrectChoiceIndex)
+      }
+      this.$bus.emit('onChangeCorrectChoiceIndexInQuestionChoicesInput', this.localCorrectChoiceIndex)
+    },
+    onEdit (choice) {
+      this.editChoice(choice)
+    },
+    onDelete (choice) {
+      this.removeChoice(choice.id)
+    },
+    editChoice (choice) {
+      const choiceIndex = this.choices.list.findIndex(item => item.id === choice.id)
+      if (choiceIndex === -1) {
+        return
+      }
+      this.entityCreateLoading = true
+      APIGateway.questionChoice.put({ data: choice })
+        .then((choice) => {
+          this.choices.list[choiceIndex].text = choice.text.replace(/<br>/g, '\n')
+          this.entityCreateLoading = false
+        })
+        .catch(() => {
+          this.entityCreateLoading = false
+        })
+    },
+    removeChoice (choiceId) {
+      const choiceIndex = this.choices.list.findIndex(item => item.id === choiceId)
+      if (choiceIndex === -1) {
+        return
+      }
+      this.choices.list.splice(choiceIndex, 1)
+      this.emitValue(this.choices.list.map(item => item.id))
+    },
+    entityCreate () {
+      this.entityCreateLoading = true
+      this.$refs.entityCreate.createEntity(false)
+        .then(response => {
+          this.choices.list.push(new QuestionChoice(response.data))
+          this.entityCreateLoading = false
+          this.emitValue(this.choices.list.map(item => item.id))
+        })
+        .catch(() => {
+          this.entityCreateLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.QuestionChoices {
+  width: 100%;
+  .choice-item {
+    display: flex;
+    margin-top: 16px;
+    .choice-item-radio {
+      width: 60px;
+    }
+    .choice-item-component {
+      width: calc( 100% - 60px );
+    }
+  }
+}
+</style>

+ 134 - 0
src/components/FormBuilderCustumComponents/QuestionsCreateAndAppend/CreateQuestionForm.vue

@@ -0,0 +1,134 @@
+<template>
+  <div class="CreateQuestionForm">
+    <entity-edit v-if="mounted"
+                 ref="entityEdit"
+                 v-model:value="inputs"
+                 title="ایجاد سوال جدید"
+                 :api="api"
+                 :loaded-data="loadedData"
+                 :entity-id-key="entityIdKey"
+                 :entity-param-key="entityParamKey"
+                 :show-route-name="showRouteName"
+                 :show-close-button="false"
+                 :show-edit-button="false"
+                 :show-expand-button="false"
+                 :show-save-button="false"
+                 :show-reload-button="false"
+                 :redirect-after-edit="false"
+                 :after-load-input-data="afterLoadInputData">
+      <template #toolbar>
+        <q-btn v-close-popup
+               icon="close"
+               flat />
+      </template>
+    </entity-edit>
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityEdit } from 'quasar-crud'
+import { Question } from 'src/models/Question.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import BtnControl from 'src/components/Control/btn.vue'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import QuestionChoices from 'src/components/FormBuilderCustumComponents/QuestionChoices/QuestionChoices.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const QuestionChoicesComp = shallowRef(QuestionChoices)
+
+export default {
+  name: 'CreateQuestionForm',
+  components: {
+    EntityEdit
+  },
+  props: {
+    testId: {
+      type: Number,
+      default: null
+    },
+    testSetId: {
+      type: Number,
+      default: null
+    },
+    questionType: {
+      default: 'QUESTION_BANK',
+      type: String
+    }
+  },
+  emits: ['onQuestionCreated'],
+  data () {
+    return {
+      mounted: false,
+      entityLoading: true,
+      api: null,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Unit.Questions.Show',
+      loadedData: new Question(),
+      inputs: [
+        { type: 'input', name: 'text', responseKey: 'text', label: 'سوال', placeholder: ' ', col: 'col-12' },
+        { type: 'inputEditor', name: 'correct_answer', responseKey: 'correct_answer', label: 'پاسخ', placeholder: ' ', col: 'col-12' },
+        { type: 'input', name: 'mark', responseKey: 'mark', label: 'بارم پیشنهادی سوال', placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'select', name: 'level', responseKey: 'level', label: 'سطح سوال', placeholder: ' ', options: (new Question()).levelEnums, col: 'col-md-3 col-12' },
+        { type: QuestionChoicesComp, name: 'choices', responseKey: 'choices_info', correctChoiceIndex: 0, col: 'col-12 flex justify-end' },
+        { type: BtnControlComp, name: 'btn', label: 'ذخیره سوال', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' },
+        { type: 'hidden', name: 'correct_choice_index', responseKey: 'correct_choice_index' },
+        { type: 'hidden', name: 'type', responseKey: 'type', value: 'QUESTION_BANK' }
+      ]
+    }
+  },
+  created() {
+    if (this.testId) {
+      this.api = APIGateway.test.APIAdresses.appendQuestion(this.testId)
+    } else if (this.testSetId) {
+      this.api = APIGateway.testSet.APIAdresses.appendQuestion(this.testSetId)
+    }
+  },
+  mounted() {
+    this.setInputs()
+    this.mounted = true
+    this.$bus.on('onChangeCorrectChoiceIndexInQuestionChoicesInput', (correctChoiceIndex) => {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'correct_choice_index', 'value', correctChoiceIndex)
+    })
+  },
+  methods: {
+    setInputs () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.edit)
+      if (this.questionType === 'QUESTION_BANK') {
+        FormBuilderAssist.setAttributeByName(this.inputs, 'choices', 'type', 'hidden')
+        FormBuilderAssist.setAttributeByName(this.inputs, 'choices', 'ignoreValue', true)
+      }
+      if (this.questionType === 'EVENT') {
+        FormBuilderAssist.setAttributeByName(this.inputs, 'mark', 'type', 'hidden')
+        FormBuilderAssist.setAttributeByName(this.inputs, 'mark', 'ignoreValue', true)
+        FormBuilderAssist.setAttributeByName(this.inputs, 'level', 'type', 'hidden')
+        FormBuilderAssist.setAttributeByName(this.inputs, 'level', 'ignoreValue', true)
+      }
+    },
+    afterLoadInputData () {
+      this.entityLoading = false
+    },
+    edit() {
+      this.entityLoading = true
+      this.$refs.entityEdit.editEntity(false)
+        .then((response) => {
+          this.$emit('onQuestionCreated', response.data)
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.CreateQuestionForm {
+  .entity-edit {
+    height: 100vh;
+    overflow: auto;
+  }
+}
+</style>

+ 120 - 0
src/components/FormBuilderCustumComponents/QuestionsCreateAndAppend/EditQuestionForm.vue

@@ -0,0 +1,120 @@
+<template>
+  <div class="EditQuestionForm">
+    <entity-edit v-if="mounted"
+                 ref="entityEdit"
+                 v-model:value="inputs"
+                 title="ویرایش سوال"
+                 :api="api"
+                 :entity-id-key="entityIdKey"
+                 :entity-param-key="entityParamKey"
+                 :show-route-name="showRouteName"
+                 :show-close-button="false"
+                 :show-edit-button="false"
+                 :show-expand-button="false"
+                 :show-save-button="false"
+                 :show-reload-button="false"
+                 :redirect-after-edit="false"
+                 :before-load-input-data="beforeLoadInputData">
+      <template #toolbar>
+        <q-btn v-close-popup
+               icon="close"
+               flat />
+      </template>
+    </entity-edit>
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityEdit } from 'quasar-crud'
+import { Question } from 'src/models/Question.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import BtnControl from 'src/components/Control/btn.vue'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import QuestionChoices from 'src/components/FormBuilderCustumComponents/QuestionChoices/QuestionChoices.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const QuestionChoicesComp = shallowRef(QuestionChoices)
+
+export default {
+  name: 'EditQuestionForm',
+  components: {
+    EntityEdit
+  },
+  props: {
+    questionId: {
+      type: Number,
+      default: null
+    },
+    questionType: {
+      default: 'QUESTION_BANK',
+      type: String
+    }
+  },
+  emits: ['onQuestionEdited'],
+  data () {
+    return {
+      mounted: false,
+      entityLoading: true,
+      api: APIGateway.question.APIAdresses.byId(this.questionId),
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Unit.Questions.Show',
+      inputs: [
+        { type: 'input', name: 'text', responseKey: 'text', label: 'سوال', placeholder: ' ', col: 'col-12' },
+        { type: 'inputEditor', name: 'correct_answer', responseKey: 'correct_answer', label: 'پاسخ', placeholder: ' ', col: 'col-12' },
+        { type: 'input', name: 'mark', responseKey: 'mark', label: 'بارم پیشنهادی سوال', placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'select', name: 'level', responseKey: 'level', label: 'سطح سوال', placeholder: ' ', options: (new Question()).levelEnums, col: 'col-md-3 col-12' },
+        { type: QuestionChoicesComp, name: 'choices', responseKey: 'choices_info', correctChoiceIndex: 0, col: 'col-12 flex justify-end' },
+        { type: BtnControlComp, name: 'btn', label: 'ذخیره سوال', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' },
+        { type: 'hidden', name: 'correct_choice_index', responseKey: 'correct_choice_index' },
+        { type: 'hidden', name: 'type', responseKey: 'type', value: 'QUESTION_BANK' }
+      ]
+    }
+  },
+  mounted() {
+    this.setInputs()
+    this.mounted = true
+    this.$bus.on('onChangeCorrectChoiceIndexInQuestionChoicesInput', (correctChoiceIndex) => {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'correct_choice_index', 'value', correctChoiceIndex)
+    })
+  },
+  methods: {
+    setInputs () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.edit)
+      if (this.questionType === 'QUESTION_BANK') {
+        FormBuilderAssist.setAttributeByName(this.inputs, 'choices', 'type', 'hidden')
+        FormBuilderAssist.setAttributeByName(this.inputs, 'choices', 'ignoreValue', true)
+      }
+      if (this.questionType === 'EVENT') {
+        FormBuilderAssist.setAttributeByName(this.inputs, 'mark', 'type', 'hidden')
+        FormBuilderAssist.setAttributeByName(this.inputs, 'level', 'type', 'hidden')
+      }
+    },
+    beforeLoadInputData (responseData) {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'choices', 'correctChoiceIndex', responseData.correct_choice_index)
+      this.entityLoading = false
+    },
+    edit() {
+      this.entityLoading = true
+      this.$refs.entityEdit.editEntity(false)
+        .then((response) => {
+          this.$emit('onQuestionEdited', response.data)
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.EditQuestionForm {
+  .entity-edit {
+    height: 100vh;
+    overflow: auto;
+  }
+}
+</style>

+ 505 - 0
src/components/FormBuilderCustumComponents/QuestionsCreateAndAppend/QuestionsCreateAndAppend.vue

@@ -0,0 +1,505 @@
+<template>
+  <div class="ContentsSelector q-mt-md">
+    <q-table :rows="testQuestionList"
+             :columns="table.columns"
+             row-key="name">
+      <template #top>
+        <q-banner class="full-width">
+          <div class="flex justify-between items-center">
+            <span>
+              سوالات
+            </span>
+            <q-btn color="primary"
+                   class="q-mr-md"
+                   outline
+                   :loading="test.loading"
+                   @click="openDialogCreateNewQuestion">
+              سوال جدید
+            </q-btn>
+          </div>
+        </q-banner>
+      </template>
+      <template #bottom />
+      <template #body-cell-number="props">
+        <q-td>
+          {{ props.rowIndex + 1 }}
+        </q-td>
+      </template>
+      <template #body-cell-mark="props">
+        <q-td auto-width>
+          <q-input v-model="props.row.mark"
+                   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"
+                    :loading="test.loading" />
+        </q-td>
+      </template>
+      <template #body-cell-action="props">
+        <q-td auto-width>
+          <q-btn color="primary"
+                 class="q-mr-md"
+                 size="sm"
+                 outline
+                 :loading="test.loading"
+                 @click="openDialogEditQuestion(props.row.question_info.id)">
+            ویرایش سوال
+          </q-btn>
+          <q-btn v-if="questionType === 'QUESTION_BANK'"
+                 color="primary"
+                 class="q-mr-lg"
+                 size="sm"
+                 outline
+                 @click="editTestQuestion(props.row)">
+            ویرایش نمره و حالت امتیازی سوال
+          </q-btn>
+          <q-btn color="red"
+                 class="q-mr-md"
+                 size="sm"
+                 :loading="test.loading"
+                 @click="deleteQuestion(props.row.question_info.id)">
+            حذف سوال
+          </q-btn>
+        </q-td>
+      </template>
+    </q-table>
+    <div class="q-mt-lg">
+      <template v-if="questionType === 'QUESTION_BANK'">
+        <div class="row">
+          <div class="col-md-4 col-12">
+            تعداد سوالات اجباری:
+            {{ countOfForceQuestions }}
+          </div>
+          <div class="col-md-4 col-12">
+            مجموع بارم‌ها:
+            {{ sumOfTotalMarks }}
+          </div>
+          <div class="col-12" />
+          <div class="col-md-4 col-12">
+            تعداد سوالات اختیاری:
+            {{ countOfExtraQuestions }}
+          </div>
+          <div class="col-md-4 col-12">
+            مجموع بارم سوالات اختیاری:
+            {{ sumOfExtraMarks }}
+          </div>
+        </div>
+      </template>
+      <template v-else-if="questionType === 'EVENT' && localValue">
+        <div class="row">
+          <div class="col-md-4 col-12">
+            تعداد سوالات انتخاب شده:
+            {{ localValue.length }}
+          </div>
+        </div>
+      </template>
+    </div>
+    <q-dialog v-model="editQuestionDialog"
+              maximized
+              transition-show="slide-up"
+              transition-hide="slide-down">
+      <edit-question-form :question-id="selectedQuestionIdForEdit"
+                          :question-type="questionType"
+                          @onQuestionEdited="onQuestionEdited" />
+    </q-dialog>
+    <q-dialog v-model="createNewQuestionDialog"
+              maximized
+              transition-show="slide-up"
+              transition-hide="slide-down">
+      <create-question-form :test-id="testId"
+                            :test-set-id="testSetId"
+                            :question-type="questionType"
+                            @onQuestionCreated="onQuestionCreated" />
+    </q-dialog>
+  </div>
+</template>
+
+<script>
+import { Test } from 'src/models/Test.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import EditQuestionForm from './EditQuestionForm.vue'
+import CreateQuestionForm from './CreateQuestionForm.vue'
+import NormalizeNumber from 'src/assets/js/NormalizeNumber.js'
+import { Question, QuestionList } from 'src/models/Question.js'
+import { SessionTemplateList } from 'src/models/SessionTemplate.js'
+
+export default {
+  name: 'QuestionsSelector',
+  components: {
+    EditQuestionForm,
+    CreateQuestionForm
+  },
+  props: {
+    value: {
+      default: () => [],
+      type: [Array, String, Number, Boolean]
+    },
+    readonly: {
+      default: false,
+      type: Boolean
+    },
+    showPageLink: {
+      default: true,
+      type: Boolean
+    },
+    showDownloadLink: {
+      default: false,
+      type: Boolean
+    },
+    questionType: {
+      default: 'QUESTION_BANK',
+      type: String
+    },
+    testId: {
+      default: null,
+      type: [Number, String, Boolean]
+    },
+    testSetId: {
+      type: Number,
+      default: null
+    },
+    unitId: {
+      default: null,
+      type: [Number, String, Boolean]
+    }
+  },
+  emits: ['update:value'],
+  data () {
+    return {
+      mounted: false,
+      selectedQuestionIdForEdit: null,
+      editQuestionDialog: false,
+      createNewQuestionDialog: false,
+      selectedSession: null,
+      test: new Test(),
+      testQuestionList: [],
+      sessions: new SessionTemplateList(),
+      sessionQuestions: new QuestionList(),
+      loadedQuestions: {},
+
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [
+        { type: 'hidden', name: 'type', value: this.questionType },
+        { type: 'hidden', name: 'session_template_questions__session_template', value: null }
+        // { type: 'hidden', name: 'session_template_questions__session_template__unit', value: this.unitId }
+      ],
+      table: {
+        columns: []
+      }
+    }
+  },
+  computed: {
+    selectedSessionTitle () {
+      if (!this.selectedSession?.title) {
+        return ''
+      }
+      return this.selectedSession?.title
+    },
+    countOfForceQuestions () {
+      return this.testQuestionList.filter(item => !item.is_extra_mark).length
+    },
+    countOfExtraQuestions () {
+      return this.testQuestionList.filter(item => item.is_extra_mark).length
+    },
+    sumOfExtraMarks () {
+      let sum = 0
+      this.testQuestionList.filter(item => item.is_extra_mark).forEach(item => {
+        sum += parseInt(NormalizeNumber.toEnglish(item.mark.toString()))
+      })
+
+      return sum
+    },
+    sumOfTotalMarks () {
+      let sum = 0
+      this.testQuestionList.forEach(item => {
+        sum += parseInt(NormalizeNumber.toEnglish(item.mark.toString()))
+      })
+
+      return sum
+    },
+    localValue: {
+      get () {
+        return this.value
+      },
+      set (newValue) {
+        this.$emit('update:value', newValue)
+      }
+    },
+    sessionOptions () {
+      return this.sessions.list.map(item => {
+        return {
+          value: item,
+          label: item.title
+        }
+      })
+    },
+    selectedSessionId () {
+      if (!this.selectedSession) {
+        return null
+      }
+      return this.selectedSession.id
+    }
+  },
+  watch: {
+    value (newValue) {
+      this.reFreshTestQuestionList()
+    }
+  },
+  created () {
+    if (this.questionType === 'QUESTION_BANK') {
+      this.table.columns = [
+        {
+          name: 'number',
+          required: true,
+          label: 'شماره',
+          align: 'left',
+          field: () => ''
+        },
+        {
+          name: 'id',
+          required: true,
+          label: 'شناسه',
+          align: 'left',
+          field: row => row.question_info.id
+        },
+        {
+          name: 'text',
+          required: true,
+          label: 'عنوان سوال',
+          align: 'left',
+          field: row => row.question_info.text
+        },
+        {
+          name: 'level',
+          required: true,
+          label: 'سطح سوال',
+          align: 'left',
+          field: row => new Question(row.question_info).level_info.label
+        },
+        {
+          name: 'mark',
+          required: true,
+          label: 'بارم',
+          align: 'left',
+          field: row => row.question_info.mark
+        },
+        {
+          name: 'is_extra_mark',
+          required: true,
+          label: 'سوال امتیازی',
+          align: 'left',
+          field: row => ''
+        },
+        {
+          name: 'action',
+          required: true,
+          label: 'عملیات',
+          align: 'left',
+          field: row => ''
+        }
+      ]
+    } else if (this.questionType === 'EVENT') {
+      this.table.columns = [
+        {
+          name: 'number',
+          required: true,
+          label: 'شماره',
+          align: 'left',
+          field: () => ''
+        },
+        {
+          name: 'id',
+          required: true,
+          label: 'شناسه',
+          align: 'left',
+          field: row => row.question_info.id
+        },
+        {
+          name: 'text',
+          required: true,
+          label: 'عنوان سوال',
+          align: 'left',
+          field: row => row.question_info.text
+        },
+        {
+          name: 'action',
+          required: true,
+          label: 'عملیات',
+          align: 'left',
+          field: row => ''
+        }
+      ]
+    }
+    this.reFreshTestQuestionList()
+  },
+  mounted () {
+    if (this.questionType === 'QUESTION_BANK') {
+      this.getSessions()
+    } else {
+      this.mounted = true
+    }
+  },
+  methods: {
+    onQuestionEdited () {
+      this.editQuestionDialog = false
+      this.refreshTestQuestions()
+    },
+    onQuestionCreated () {
+      this.createNewQuestionDialog = false
+      this.refreshTestQuestions()
+    },
+    openDialogEditQuestion (questionId) {
+      this.selectedQuestionIdForEdit = questionId
+      this.editQuestionDialog = true
+    },
+    openDialogCreateNewQuestion () {
+      this.createNewQuestionDialog = true
+    },
+    getShowQuestionRoute (questionId, unitId, selectedSessionId) {
+      if (this.questionType === 'QUESTION_BANK') {
+        return { name: 'Admin.Unit.Questions.Show', params: { unit_id: unitId, session_template_id: selectedSessionId, id: questionId } }
+      } else if (this.questionType === 'EVENT') {
+        return { name: 'Admin.Event.TestSet.Questions.Show', params: { question_id: questionId } }
+      }
+    },
+    emitValues () {
+
+    },
+    reFreshTestQuestionList () {
+      if (!this.localValue || !Array.isArray(this.localValue)) {
+        return
+      }
+      this.testQuestionList = JSON.parse(JSON.stringify(this.localValue))
+    },
+    editTestQuestion (testQuestion) {
+      this.test.loading = true
+      APIGateway.test.editTestQuestion(testQuestion)
+        .then(() => {
+          this.test.loading = false
+          this.refreshTestQuestions()
+        })
+        .catch(() => {
+          this.test.loading = false
+        })
+    },
+    deleteQuestion (questionId) {
+      this.$q.dialog({
+        title: 'حذف سوال',
+        message: 'آیا از حذف سوال اطمینان دارید؟',
+        cancel: true,
+        persistent: true
+      }).onOk(() => {
+        this.test.loading = true
+        APIGateway.question.delete({ data: { id: questionId } })
+          .then(() => {
+            this.test.loading = false
+            this.refreshTestQuestions()
+          })
+          .catch(() => {
+            this.test.loading = false
+          })
+      }).onCancel(() => {})
+    },
+    refreshTestQuestions () {
+      this.test.loading = true
+      if (this.testId) {
+        APIGateway.test.get(this.testId)
+          .then((test) => {
+            this.test.loading = false
+            this.localValue = test.inputData.test_questions_info
+            this.reFreshTestQuestionList()
+          })
+          .catch(() => {
+            this.test.loading = false
+          })
+      } else if (this.testSetId) {
+        APIGateway.testSet.get(this.testSetId)
+          .then((testSet) => {
+            this.test.loading = false
+            this.localValue = testSet.inputData.test_set_questions
+            this.reFreshTestQuestionList()
+          })
+          .catch(() => {
+            this.test.loading = false
+          })
+      }
+    },
+    filterQuestionList () {
+      if (this.selectedSessionId) {
+        FormBuilderAssist.setAttributeByName(this.inputs, 'session_template_questions__session_template', 'value', this.selectedSessionId)
+      }
+
+      this.$refs.entityIndexQuestions.search()
+    },
+    getSessions () {
+      this.sessions.loading = true
+      APIGateway.sessionTemplate.index({ unit: this.unitId || null, per_page: 999999 })
+        .then((data) => {
+          this.sessions = new SessionTemplateList(data.list)
+          if (this.sessions.list.length > 0) {
+            // this.selectedSession = this.sessions.list[0]
+          }
+          this.filterQuestionList()
+          this.sessions.loading = false
+          this.mounted = true
+        })
+        .catch(() => {
+          this.sessions.loading = false
+          this.mounted = true
+        })
+    },
+    getSessionQuestions () {
+      this.sessions.loading = true
+      APIGateway.sessionTemplate.index({ unit: this.unitId || null, per_page: 999999 })
+        .then((data) => {
+          this.sessions = new SessionTemplateList(data.list)
+          if (this.sessionOptions.length > 0) {
+            this.selectedSession = this.sessionOptions[0]
+          }
+          this.sessions.loading = false
+        })
+        .catch(() => {
+          this.sessions.loading = false
+        })
+    },
+    onChangeQuestionListPage (response) {
+      this.updateloadedQuestionsByArray(response.data.results)
+    },
+    updateloadedQuestionsByArray (array, allSelected = false) {
+      if (!array || !Array.isArray(array)) {
+        return
+      }
+      array.forEach(item => {
+        const questionId = item.question || item.id
+        const mark = item.mark || 0
+        const isExtraMark = item.is_extra_mark || false
+        const selected = allSelected || this.value.includes(questionId)
+        const target = this.loadedQuestions[questionId]
+        if (!target) {
+          this.loadedQuestions[questionId] = {
+            question: questionId,
+            mark,
+            is_extra_mark: isExtraMark,
+            selected
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.ContentsSelector {
+}
+</style>

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

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

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

@@ -0,0 +1,254 @@
+<template>
+  <div class="UsersOfTestSelector q-mt-md">
+    <q-banner>
+      لیست کاربران
+    </q-banner>
+    <div class="content-list q-mt-lg">
+      <entity-index v-if="mounted"
+                    ref="entityIndexUsers"
+                    v-model:value="inputs"
+                    :title="'لیست کاربران'"
+                    :api="api"
+                    :table="table"
+                    :table-keys="tableKeys"
+                    :show-search-button="false"
+                    :show-expand-button="false"
+                    :show-reload-button="false"
+                    @onPageChanged="onChangeQuestionListPage">
+        <template #toolbar>
+          <q-btn color="primary"
+                 class="q-mr-md"
+                 outline
+                 @click="selectAllAllowedUser">
+            انتخاب تمام افراد مجاز
+          </q-btn>
+          <q-btn color="primary"
+                 icon="search"
+                 @click="searchList" />
+        </template>
+        <template #entity-index-table-cell="{inputData}">
+          <template v-if="inputData.col.name === 'choice'">
+            <q-checkbox v-model="loadedItems[inputData.props.row.owner].selected"
+                        color="primary"
+                        @update:model-value="emitValues" />
+            {{ inputData.col.value }}
+          </template>
+          <template v-else-if="inputData.col.name === 'number'">
+            {{ inputData.rowNumber }}
+          </template>
+          <template v-else>
+            {{ inputData.col.value }}
+          </template>
+        </template>
+      </entity-index>
+    </div>
+    <div class="content-list q-mt-lg">
+      <div class="row">
+        <div class="col-md-4 col-12">
+          تعداد انتخاب شده:
+          {{ selectedItems.length }}
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { EntityIndex } from 'quasar-crud'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'UsersOfTestSelector',
+  components: {
+    EntityIndex
+  },
+  props: {
+    value: {
+      default: () => [],
+      type: [Array, String, Number, Boolean]
+    },
+    readonly: {
+      default: false,
+      type: Boolean
+    },
+    showPageLink: {
+      default: true,
+      type: Boolean
+    },
+    showDownloadLink: {
+      default: false,
+      type: Boolean
+    },
+    classroomId: {
+      default: null,
+      type: [Number, String]
+    }
+  },
+  emits: ['update:value'],
+  data () {
+    return {
+      loadedItems: {},
+      mounted: false,
+
+      api: APIGateway.registration.APIAdresses.examineeSheet,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [
+        { type: 'input', name: 'owner__national_code', value: null, label: 'کد ملی', placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'input', name: 'owner__firstname', value: null, label: 'نام', placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'input', name: 'owner__lastname', value: null, label: 'نام خانوادگی', placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'hidden', name: 'per_page', value: '200' },
+        { type: 'hidden', name: 'classroom', value: this.classroomId }
+      ],
+      table: {
+        columns: [
+          {
+            name: 'choice',
+            required: true,
+            label: 'انتخاب',
+            align: 'left',
+            field: (row) => {
+              this.loadedItems[row.owner].is_passed_project_condition = row.is_passed_project_condition
+              this.loadedItems[row.owner].is_passed_assignment_condition = row.is_passed_assignment_condition
+              this.loadedItems[row.owner].is_passed_attendance_condition = row.is_passed_attendance_condition
+              return ''
+            }
+          },
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'id',
+            required: true,
+            label: 'شناسه',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'owner_info.fullName',
+            required: true,
+            label: 'نام و نام خانوادگی',
+            align: 'left',
+            field: row => row.owner_info.firstname + ' ' + row.owner_info.lastname
+          },
+          {
+            name: 'owner_info.national_code',
+            required: true,
+            label: 'کد ملی',
+            align: 'left',
+            field: row => row.owner_info.national_code
+          },
+          {
+            name: 'owner_info.mobile_number',
+            required: true,
+            label: 'شماره تلفن',
+            align: 'left',
+            field: row => row.owner_info.mobile_number
+          },
+          {
+            name: 'is_passed_project_condition',
+            required: true,
+            label: 'پروژه',
+            align: 'left',
+            field: row => row.is_passed_project_condition ? 'مجاز' : 'غیر مجاز'
+          },
+          {
+            name: 'is_passed_assignment_condition',
+            required: true,
+            label: 'تکلیف',
+            align: 'left',
+            field: row => row.is_passed_assignment_condition ? 'مجاز' : 'غیر مجاز'
+          },
+          {
+            name: 'is_passed_attendance_condition',
+            required: true,
+            label: 'حضور و غیاب',
+            align: 'left',
+            field: row => row.is_passed_attendance_condition ? 'مجاز' : 'غیر مجاز'
+          }
+        ]
+      }
+    }
+  },
+  computed: {
+    selectedItems () {
+      const resultArray = []
+      Object.keys(this.loadedItems).forEach(itemId => {
+        if (this.loadedItems[itemId].selected) {
+          resultArray.push(this.loadedItems[itemId])
+        }
+      })
+      return resultArray
+    },
+    localValue: {
+      get () {
+        return this.value
+      },
+      set (newValue) {
+        this.$emit('update:value', newValue)
+      }
+    }
+  },
+  watch: {
+    value (newValue) {
+      this.updateloadedItemsByArray(newValue, true)
+    }
+  },
+  mounted() {
+    this.mounted = true
+  },
+  methods: {
+    selectAllAllowedUser () {
+      Object.keys(this.loadedItems)
+        .forEach(itemId => {
+          if (
+            this.loadedItems[itemId].is_passed_project_condition &&
+            this.loadedItems[itemId].is_passed_assignment_condition &&
+            this.loadedItems[itemId].is_passed_attendance_condition
+          ) {
+            this.loadedItems[itemId].selected = true
+          }
+        })
+      this.emitValues()
+    },
+    searchList () {
+      this.$refs.entityIndexUsers.search()
+    },
+    emitValues () {
+      this.localValue = this.selectedItems.map(item => item.id)
+    },
+    onChangeQuestionListPage (response) {
+      this.updateloadedItemsByArray(response.data.results)
+    },
+    updateloadedItemsByArray (array, allSelected = false) {
+      array.forEach(item => {
+        const itemId = item.owner || item.id || item
+        const selected = allSelected || this.value.includes(itemId)
+        const target = this.loadedItems[itemId]
+        if (!target) {
+          this.loadedItems[itemId] = {
+            id: itemId,
+            selected
+          }
+        } else {
+          this.loadedItems[itemId].selected = selected
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.UsersOfTestSelector {
+}
+</style>

+ 6 - 0
src/components/Template/SideBard/AdminPanelSideBar.vue

@@ -74,6 +74,12 @@ export default {
               routeName: 'Admin.Event.Index',
               show: true,
               active: false
+            },
+            {
+              title: 'پرسش نامه ها',
+              routeName: 'Admin.Event.TestSet.Index',
+              show: true,
+              active: false
             }
           ]
         },

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

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

+ 433 - 0
src/components/Widgets/Classroom/AdminClassroomAnswerBookList/AdminClassroomAnswerBookList.vue

@@ -0,0 +1,433 @@
+<template>
+  <div class="AdminClassroomAnswerBookList">
+    <q-skeleton v-if="!classroom.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-index v-if="mounted"
+                  ref="entityIndex"
+                  v-model:value="inputs"
+                  :title="'لیست ' + classroomTypeTitle"
+                  :api="api"
+                  :table="table"
+                  :table-keys="tableKeys"
+                  :show-expand-button="false"
+                  :show-reload-button="false"
+                  :show-search-button="false"
+                  @onPageChanged="onPageChanged">
+      <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="$route.query.classroom_type === 'EVENT' ? 'مشاهده پرسشنامه' : inputData.props.row.grader ? 'ویرایش تصحیح' : 'تصحیح آزمون'"
+                   :to="{name: 'Admin.Classroom.Test.AnswerBooks.ShowGrade', params: {classroom_id: $route.params.classroom_id, test_id: $route.params.test_id, answer_book_id: inputData.props.row.id}, query: { classroom_type: $route.query.classroom_type}}"
+                   class="q-mr-md" />
+          </div>
+        </template>
+        <template v-else>
+          {{ inputData.col.value }}
+        </template>
+      </template>
+    </entity-index>
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityIndex } from 'quasar-crud'
+import { Test } from 'src/models/Test.js'
+import Enums from 'src/assets/Enums/Enums.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { Classroom } from 'src/models/Classroom.js'
+import { AnswerBook } from 'src/models/AnswerBook.js'
+// import ShamsiDate from 'src/assets/ShamsiDate.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import BtnControl from 'src/components/Control/btn.vue'
+import { UnitCategory } from 'src/models/UnitCategory.js'
+import { mixinWidget, mixinAuth } from 'src/mixin/Mixins.js'
+import Breadcrumbs from 'src/components/Widgets/Breadcrumbs/Breadcrumbs.vue'
+import Assist from 'assets/js/Assist'
+
+const BtnControlComp = shallowRef(BtnControl)
+
+export default {
+  name: 'AdminClassroomAnswerBookList',
+  components: {
+    EntityIndex,
+    Breadcrumbs
+  },
+  mixins: [mixinWidget, mixinAuth],
+  data () {
+    return {
+      mounted: false,
+      entityLoading: true,
+      test: new Test(),
+      classroom: new Classroom(),
+      defaultOptions: {
+        classroomType: 'TRAINING'
+      },
+      api: APIGateway.answerBook.APIAdresses.base,
+      inputs: [],
+      eventInputs: [
+        {
+          type: 'select',
+          name: 'registration__status',
+          options: [
+            {
+              label: 'ثبت نام',
+              value: 'REGISTERED'
+            },
+            {
+              label: 'حذف توسط کاربر',
+              value: 'DROPPED_BY_ITSELF'
+            },
+            {
+              label: 'حذف توسط ادمین',
+              value: 'DROPPED_BY_ADMIN'
+            }
+          ],
+          label: 'وضعیت ثبت نام',
+          placeholder: ' ',
+          value: null,
+          col: 'col-md-3 col-12'
+        },
+        { type: 'input', name: 'owner__national_code', label: 'کدملی', placeholder: ' ', col: ' col-md-3 col-12' },
+        { type: 'hidden', name: 'test', value: this.$route.params.test_id },
+        { type: 'hidden', name: 'test__classroom', value: this.$route.params.classroom_id },
+        { type: BtnControlComp, name: 'btn', label: 'جستجو', placeholder: ' ', atClick: () => {}, col: 'col-md-2 col-12' }
+      ],
+      classroomInputs: [
+        {
+          type: 'select',
+          name: 'status',
+          options: [
+            { label: 'جاری', value: 'WAITING_FOR_GRADING' },
+            { label: 'در حال تصحیح', value: 'GRADING' },
+            { label: 'تصحیح شده', value: 'GRADED' },
+            {
+              label: 'حذف شده',
+              value: 'DELETED'
+            },
+            {
+              label: 'نیاز به رفع مشکل',
+              value: 'UNGRADABLE'
+            },
+            {
+              label: 'اعتراض شده',
+              value: 'OBJECTED'
+            }
+          ],
+          label: 'وضعیت',
+          placeholder: ' ',
+          value: null,
+          col: 'col-md-3 col-12'
+        },
+        { type: 'input', name: 'owner__national_code', label: 'کدملی', placeholder: ' ', col: ' col-md-3 col-12' },
+        { type: 'hidden', name: 'test', value: this.$route.params.test_id },
+        { type: 'hidden', name: 'test__classroom', value: this.$route.params.classroom_id },
+        { type: BtnControlComp, name: 'btn', label: 'جستجو', placeholder: ' ', atClick: () => {}, col: 'col-md-2 col-12' }
+      ],
+      table: {
+        columns: []
+      },
+      eventColumns: [
+        {
+          name: 'number',
+          required: true,
+          label: 'شماره',
+          align: 'left',
+          field: () => ''
+        },
+        {
+          name: 'id',
+          required: true,
+          label: 'شناسه',
+          align: 'left',
+          field: row => row.id
+        },
+        {
+          name: 'owner_info.fullname',
+          required: true,
+          label: 'نام و نام خانوادگی',
+          align: 'left',
+          field: row => row.owner_info.firstname + ' ' + row.owner_info.lastname
+        },
+        {
+          name: 'owner_info.national_code',
+          required: true,
+          label: 'کد ملی',
+          align: 'left',
+          field: row => row.owner_info.national_code
+        },
+        {
+          name: 'actions',
+          required: true,
+          label: 'عملیات',
+          align: 'left',
+          field: ''
+        }
+      ],
+      classroomColumns: [
+        {
+          name: 'number',
+          required: true,
+          label: 'شماره',
+          align: 'left',
+          field: () => ''
+        },
+        {
+          name: 'id',
+          required: true,
+          label: 'شناسه',
+          align: 'left',
+          field: row => row.id
+        },
+        {
+          name: 'owner_info.fullname',
+          required: true,
+          label: 'نام و نام خانوادگی',
+          align: 'left',
+          field: row => row.owner_info.firstname + ' ' + row.owner_info.lastname
+        },
+        {
+          name: 'owner_info.national_code',
+          required: true,
+          label: 'کد ملی',
+          align: 'left',
+          field: row => row.owner_info.national_code
+        },
+        {
+          name: 'attending_start_time',
+          required: true,
+          label: 'زمان شروع',
+          align: 'left',
+          field: row => row.attending_start_time ? this.toShamsi(row.attending_start_time) : '-'
+        },
+        {
+          name: 'attending_start_time',
+          required: true,
+          label: 'زمان پایان',
+          align: 'left',
+          field: row => row.attending_end_time ? this.toShamsi(row.attending_end_time) : '-'
+        },
+        {
+          name: 'grader_info',
+          required: true,
+          label: 'مصحح',
+          align: 'left',
+          field: row => row.grader_info ? row.grader_info.firstname + ' ' + row.grader_info.lastname : '-'
+        },
+        // {
+        //   name: 'last_modification_time',
+        //   required: true,
+        //   label: 'زمان آخرین ویرایش تصحیح',
+        //   align: 'left',
+        //   field: row => row.last_modification_time ? this.toShamsi(row.last_modification_time) : '-'
+        // },
+        // {
+        //   name: 'title',
+        //   required: true,
+        //   label: 'عنوان آزمون',
+        //   align: 'left',
+        //   field: row => row.test_info.title
+        // },
+        // {
+        //   name: 'test_questions_length',
+        //   required: true,
+        //   label: 'تعداد سوالات',
+        //   align: 'left',
+        //   field: row => row.test_info.test_questions_length
+        // },
+        {
+          name: 'status_info',
+          required: true,
+          label: 'وضعیت تصحیح',
+          align: 'left',
+          field: row => (new AnswerBook(row)).status_info.label
+        },
+        {
+          name: 'test_info_status_info',
+          required: true,
+          label: 'وضعیت آزمون',
+          align: 'left',
+          field: row => (new Test(row.test_info)).status_info.label
+        },
+        {
+          name: 'total_score',
+          required: true,
+          label: 'نمره آزمون',
+          align: 'left',
+          field: row => row.total_score
+        },
+        {
+          name: 'actions',
+          required: true,
+          label: 'عملیات',
+          align: 'left',
+          field: ''
+        }
+      ],
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      }
+    }
+  },
+  computed: {
+    classroomTypeTitle () {
+      const unitCategory = new UnitCategory({ type: this.localOptions.classroomType })
+      return unitCategory.type_info.label
+    },
+    createPageRouteName () {
+      if (this.localOptions.classroomType === 'TRAINING') {
+        return 'Admin.Classroom.Create'
+      }
+      if (this.localOptions.classroomType === 'EVENT') {
+        return 'Admin.Event.Create'
+      }
+      return 'Admin.Classroom.Create'
+    },
+    showPageRouteName () {
+      if (this.localOptions.classroomType === 'TRAINING') {
+        return 'Admin.Classroom.Show'
+      }
+      if (this.localOptions.classroomType === 'EVENT') {
+        return 'Admin.Event.Show'
+      }
+      return 'Admin.Classroom.Show'
+    }
+  },
+  mounted () {
+    this.setClassroomTypeOfInputs()
+    this.mounted = true
+  },
+  methods: {
+    toShamsi (miladi) {
+      return Assist.miladiToShamsi(miladi)
+    },
+    onPageChanged () {
+      if (!this.classroom.id) {
+        this.getClassroom()
+      }
+    },
+    getClassroom () {
+      this.classroom.loading = true
+      APIGateway.classroom.get(this.$route.params.classroom_id)
+        .then((classroom) => {
+          this.classroom = new Classroom(classroom)
+          this.classroom.loading = false
+          this.updateBreadcrumbs()
+        })
+        .catch(() => {
+          this.classroom.loading = false
+        })
+    },
+    updateBreadcrumbs () {
+      this.$store.commit('AppLayout/updateBreadcrumbs', {
+        visible: true,
+        loading: false,
+        path: [
+          {
+            label: 'لیست ' + this.classroomTypeTitle,
+            to: { name: this.indexPageRouteName }
+          },
+          {
+            label: this.classroom.title,
+            to: { name: this.showPageRouteName, params: { id: this.$route.params.classroom_id } }
+          },
+          {
+            label: 'تصحیح آزمون'
+          },
+          {
+            label: 'لیست شرکت کنندگان'
+          }
+        ]
+      })
+    },
+    setActionBtn () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.search)
+    },
+    setClassroomTypeOfInputs () {
+      if (this.$route.query.classroom_type === 'EVENT') {
+        this.inputs = this.eventInputs
+        this.table.columns = this.eventColumns
+      } else {
+        this.inputs = this.classroomInputs
+        this.table.columns = this.classroomColumns
+      }
+      this.setActionBtn()
+      this.table.columns.forEach(col => {
+        if (col.name === 'title') {
+          col.label = 'نام ' + this.classroomTypeTitle
+        }
+        if (col.name === 'professor_info') {
+          col.label = (this.localOptions.classroomType === 'TRAINING') ? 'استاد مربوطه' : 'برگزار کننده'
+        }
+      })
+    },
+    search () {
+      this.$refs.entityIndex.search()
+    },
+    getSelectOptions (result, value, label) {
+      return result.map(item => {
+        return {
+          value: item[value],
+          label: item[label]
+        }
+      })
+    },
+    getRemoveMessage (row) {
+      return 'آیا از حذف ' + row.title + ' اطمینان دارید؟'
+    },
+    getClassroomHoldingTypeTitle (type) {
+      const target = Enums.classroomHoldingTypes.find(item => item.value === type)
+      if (!target) {
+        return '-'
+      }
+
+      return target.label
+    },
+    getClassroomStatusesTitle (type) {
+      const target = Enums.classroomStatuses.find(item => item.value === type)
+      if (!target) {
+        return '-'
+      }
+
+      return target.label
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.AdminClassroomAnswerBookList {
+  .add-new-action-row {
+    display: flex;
+    flex-flow: row;
+    align-items: center;
+    justify-content: flex-end;
+    margin-bottom: 22px;
+  }
+  .action-column-entity-index {
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+  }
+}
+</style>

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

@@ -0,0 +1,28 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      <q-option-group v-model="localOptions.classroomType"
+                      :options="classroomTypeOptions" />
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { UnitCategory } from 'src/models/UnitCategory.js'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel],
+  data () {
+    return {
+      classroomTypeOptions: (new UnitCategory()).typeEnums
+    }
+  }
+})
+</script>

+ 17 - 11
src/components/Widgets/Classroom/AdminClassroomShow/AdminClassroomShow.vue

@@ -32,7 +32,7 @@
              label="پروژه‌ها" />
       <q-tab v-if="canShowTabPage('exams')"
              name="exams"
-             label="آزمون" />
+             :label="localOptions.classroomType === 'TRAINING' ? 'آزمون' : 'پرسشنامه'" />
       <q-tab v-if="canShowTabPage('members')"
              name="members"
              label="اندیشه جویان" />
@@ -61,40 +61,43 @@
                    class="q-pa-none">
         <classroom-info v-model:classroom="classroom"
                         :classroom-type="localOptions.classroomType"
-                        :classroom-id="$route.params.id" />
+                        :classroom-id="parseInt($route.params.id)" />
       </q-tab-panel>
       <q-tab-panel name="sessions"
                    class="q-pa-none">
-        <session-list :classroom-id="$route.params.id"
+        <session-list :classroom-id="parseInt($route.params.id)"
                       :classroom-type="localOptions.classroomType" />
       </q-tab-panel>
 
       <q-tab-panel name="projects"
                    class="q-pa-none">
-        <project-list :classroom-id="$route.params.id" />
+        <project-list :classroom-id="parseInt($route.params.id)" />
       </q-tab-panel>
-      <q-tab-panel name="exams">
-        آزمون
+      <q-tab-panel name="exams"
+                   class="q-pa-none">
+        <test-list :classroom="classroom"
+                   :classroom-id="parseInt($route.params.id)"
+                   :classroom-type="localOptions.classroomType" />
       </q-tab-panel>
       <q-tab-panel name="members"
                    class="q-pa-none">
-        <members-list :classroom-id="$route.params.id" />
+        <members-list :classroom-id="parseInt($route.params.id)" />
       </q-tab-panel>
       <q-tab-panel name="teams"
                    class="q-pa-none">
-        <team-list :classroom-id="$route.params.id" />
+        <team-list :classroom-id="parseInt($route.params.id)" />
       </q-tab-panel>
       <q-tab-panel name="graders"
                    class="q-pa-none">
-        <grader-list :classroom-id="$route.params.id" />
+        <grader-list :classroom-id="parseInt($route.params.id)" />
       </q-tab-panel>
       <q-tab-panel name="leaders"
                    class="q-pa-none">
-        <leader-list :classroom-id="$route.params.id" />
+        <leader-list :classroom-id="parseInt($route.params.id)" />
       </q-tab-panel>
       <q-tab-panel name="activity_sheet"
                    class="q-pa-none">
-        <activity-sheet-list :classroom-id="$route.params.id" />
+        <activity-sheet-list :classroom-id="parseInt($route.params.id)" />
       </q-tab-panel>
       <q-tab-panel name="live_streaming_url">
         <live-streaming :classroom="classroom" />
@@ -105,6 +108,7 @@
 
 <script>
 import TeamList from './components/TeamList.vue'
+import TestList from './components/TestList.vue'
 import { mixinWidget } from 'src/mixin/Mixins.js'
 import { Classroom } from 'src/models/Classroom.js'
 import LeaderList from './components/LeaderList.vue'
@@ -122,6 +126,7 @@ export default {
   name: 'Admin.Classroom.Show',
   components: {
     TeamList,
+    TestList,
     LeaderList,
     GraderList,
     SessionList,
@@ -194,6 +199,7 @@ export default {
             'classroomInfo',
             'sessions',
             'members',
+            'exams',
             'live_streaming_url'
           ]
         }

+ 16 - 8
src/components/Widgets/Classroom/AdminClassroomShow/components/GraderList.vue

@@ -131,6 +131,20 @@ export default {
                 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
               }
             ]
           },
@@ -141,7 +155,8 @@ export default {
             { 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: 'hidden', name: 'role', value: 'team_grader' },
+            { type: 'input', name: 'search', value: null, label: 'جست و جو', placeholder: ' ', col: 'col-md-3 col-12' },
+            { type: 'hidden', name: 'role', value: 'grader' },
             { type: 'hidden', name: 'classroom_graders__classroom__ne', value: classroomId }
           ],
           itemIndicatorKey: row => row.firstname + ' ' + row.lastname,
@@ -175,13 +190,6 @@ export default {
             align: 'left',
             field: row => row.grader_info.id
           },
-          {
-            name: 'title',
-            required: true,
-            label: 'جنسیت',
-            align: 'left',
-            field: row => (row.grader_info.gender === 1) ? 'مذکر' : ((row.grader_info.gender === 2) ? 'مونث' : '-')
-          },
           {
             name: 'creation_time',
             required: true,

+ 8 - 11
src/components/Widgets/Classroom/AdminClassroomShow/components/MembersList.vue

@@ -20,18 +20,15 @@
       <template v-if="inputData.col.name === 'number'">
         {{ inputData.rowNumber }}
       </template>
-      <template v-if="inputData.col.name === 'UserRegisterStatus'">
-        <q-btn v-if="inputData.props.row.status === 'DROPPED_FOR_CONDITION'"
-               color="primary"
-               class="q-mr-md"
-               :loading="revertDroppedForConditionLoading"
-               @click="revertDroppedForCondition(inputData.props.row.id)">
-          بازگرداندن ثبت نام
-        </q-btn>
-        {{ inputData.col.value }}
-      </template>
       <template v-else-if="inputData.col.name === 'action'">
         <div class="action-column-entity-index">
+          <q-btn v-if="inputData.props.row.status === 'DROPPED_FOR_CONDITION'"
+                 color="primary"
+                 class="q-mr-md"
+                 :loading="revertDroppedForConditionLoading"
+                 @click="revertDroppedForCondition(inputData.props.row.id)">
+            بازگرداندن ثبت نام
+          </q-btn>
           <q-btn v-if="inputData.props.row.status !== 'DROPPED_BY_ADMIN' && inputData.props.row.status !== 'DROPPED_BY_ITSELF'"
                  size="md"
                  color="red"
@@ -150,7 +147,7 @@ export default {
             required: true,
             label: 'وضعیت',
             align: 'left',
-            field: row => (new Classroom()).getUserRegisterInfoLabel(row.status)
+            field: row => (new Registration(row)).status_info.label
           },
           {
             name: 'action',

+ 213 - 0
src/components/Widgets/Classroom/AdminClassroomShow/components/TestList.vue

@@ -0,0 +1,213 @@
+<template>
+  <entity-index v-if="mounted"
+                ref="testList"
+                v-model:value="testListInputs"
+                :title="listTitle"
+                :api="testListApi"
+                :table="testListTable"
+                :table-keys="testListTableKeys"
+                :show-search-button="false"
+                :show-reload-button="false"
+                :show-expand-button="false">
+    <template #toolbar>
+      <q-btn color="primary"
+             outline
+             :loading="addNewSessionLoading"
+             :to="{name: 'Admin.Classroom.TestSet.Index', params: {classroom_id: classroomId, unit_id: classroom.unit}, query: { classroom_type: classroomType}}">
+        {{ addBtnTitle }}
+      </q-btn>
+    </template>
+    <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 === 'answer'">
+        <div class="action-column-entity-index">
+          <q-btn size="md"
+                 outline
+                 color="grey"
+                 label="تصحیح"
+                 :to="{name: 'Admin.Classroom.Test.AnswerBooks.Index', params: {classroom_id: classroomId, test_id: inputData.props.row.id}, query: { classroom_type: classroomType}}" />
+        </div>
+      </template>
+      <template v-else-if="inputData.col.name === 'actions'">
+        <div class="action-column-entity-index">
+          <q-btn size="md"
+                 color="primary"
+                 label="جزییات"
+                 :to="{name: 'Admin.Classroom.TestSet.Test.Show', params: {classroom_id: classroomId, test_set_id: inputData.props.row.test_set_info.id, id: inputData.props.row.id}, query: { classroom_type: classroomType}}"
+                 class="q-mr-md" />
+        </div>
+      </template>
+      <template v-else>
+        {{ inputData.col.value }}
+      </template>
+    </template>
+  </entity-index>
+  <q-skeleton v-else
+              type="rect"
+              height="200px" />
+</template>
+
+<script>
+import { EntityIndex } from 'quasar-crud'
+import { Test } from 'src/models/Test.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { Classroom } from 'src/models/Classroom'
+
+export default {
+  name: 'TestList',
+  components: {
+    EntityIndex
+  },
+  props: {
+    classroomId: {
+      type: Number,
+      default: null
+    },
+    classroomType: {
+      type: String,
+      default: 'TRAINING'
+    },
+    classroom: {
+      type: Classroom,
+      default: new Classroom()
+    }
+  },
+  data () {
+    return {
+      mounted: false,
+      listTitle: 'لیست آزمون ها',
+      addBtnTitle: 'افزودن آزمون',
+      addNewSessionLoading: false,
+
+      testListInputs: [
+        { type: 'hidden', name: 'classroom', value: this.classroomId }
+      ],
+      testListApi: APIGateway.test.APIAdresses.base,
+      testListTable: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'id',
+            required: true,
+            label: 'شناسه',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'عنوان آزمون',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'test_questions_length',
+            required: true,
+            label: 'تعداد سوالات',
+            align: 'left',
+            field: row => row.test_questions.length
+          },
+          {
+            name: 'level',
+            required: true,
+            label: 'درچه سختی',
+            align: 'left',
+            field: row => (new Test(row.test_set_info)).level_info.label
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'وضعیت',
+            align: 'left',
+            field: row => (new Test(row)).status_info.label
+          },
+          {
+            name: 'answer',
+            required: true,
+            label: 'تصحییح',
+            align: 'left',
+            field: row => ''
+          },
+          {
+            name: 'actions',
+            required: true,
+            label: 'عملیات',
+            align: 'left',
+            field: ''
+          }
+        ]
+      },
+      testListTableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      }
+    }
+  },
+  computed: {
+    sessionAttendanceSheetList () {
+      if (this.classroomType === 'TRAINING') {
+        return 'Admin.Classroom.Session.AttendanceSheetList'
+      }
+      if (this.classroomType === 'EVENT') {
+        return 'Admin.Event.Session.AttendanceSheetList'
+      }
+      return 'Admin.Classroom.Session.AttendanceSheetList'
+    }
+  },
+  mounted () {
+    this.setClassroomTypeOfInputs()
+    this.mounted = true
+  },
+  methods: {
+    setClassroomTypeOfInputs () {
+      if (this.classroomType === 'TRAINING') {
+        this.listTitle = 'لیست آزمون ها'
+        this.addBtnTitle = 'افزودن آزمون'
+      }
+      if (this.classroomType === 'EVENT') {
+        this.listTitle = 'لیست پرسشنامه ها'
+        this.addBtnTitle = 'افزودن پرسشنامه'
+        this.testListTable.columns.forEach(col => {
+          if (col.name === 'title') {
+            col.label = 'عنوان پرسشنامه'
+          }
+          if (col.name === 'beginning_time') {
+            col.label = 'زمان شروع پرسشنامه'
+          }
+          if (col.name === 'ending_time') {
+            col.label = 'زمان پایان پرسشنامه'
+          }
+        })
+      }
+    },
+    addNewSession () {
+      this.addNewSessionLoading = true
+      APIGateway.session.create({
+        title: '-',
+        classroom: this.classroomId
+      })
+        .then(() => {
+          this.$refs.testList.search()
+          this.addNewSessionLoading = false
+        })
+        .catch(() => {
+          this.addNewSessionLoading = false
+        })
+    },
+    getRemoveMessage (row) {
+      return 'آیا از حذف ' + row.title + ' اطمینان دارید؟'
+    }
+  }
+}
+</script>

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

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

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

@@ -0,0 +1,28 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      <q-option-group v-model="localOptions.classroomType"
+                      :options="classroomTypeOptions" />
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { UnitCategory } from 'src/models/UnitCategory.js'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel],
+  data () {
+    return {
+      classroomTypeOptions: (new UnitCategory()).typeEnums
+    }
+  }
+})
+</script>

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

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

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

@@ -0,0 +1,28 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      <q-option-group v-model="localOptions.classroomType"
+                      :options="classroomTypeOptions" />
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { UnitCategory } from 'src/models/UnitCategory.js'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel],
+  data () {
+    return {
+      classroomTypeOptions: (new UnitCategory()).typeEnums
+    }
+  }
+})
+</script>

+ 1 - 1
src/components/Widgets/Classroom/ClassroomShow/ClassroomShow.vue

@@ -209,7 +209,7 @@ import { SessionAttendanceSheets } from 'src/models/SessionAttendanceSheets.js'
 import ShowClassroomInfo from 'src/components/Widgets/Other/ShowClassroomInfo/ShowClassroomInfo.vue'
 
 export default {
-  name: 'UserPanel.Profile.ClassroomInfo',
+  name: 'ClassroomShow',
   components: {
     EntityIndex,
     Breadcrumbs,

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

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

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

@@ -0,0 +1,28 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      <q-option-group v-model="localOptions.classroomType"
+                      :options="classroomTypeOptions" />
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { UnitCategory } from 'src/models/UnitCategory.js'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel],
+  data () {
+    return {
+      classroomTypeOptions: (new UnitCategory()).typeEnums
+    }
+  }
+})
+</script>

+ 115 - 46
src/components/Widgets/Other/Shop/ShopCompleteInfo/ShopCompleteInfo.vue

@@ -80,43 +80,61 @@
       </div>
     </q-card>
 
-    <q-banner class="banner complete-info-form-banner">
-      تکمیل اطلاعات
-    </q-banner>
-    <q-card class="complete-info-form-card">
-      <entity-edit v-if="mounted"
-                   ref="entityEdit"
-                   v-model:value="inputs"
-                   title="اطلاعات کاربری"
-                   :api="api"
-                   :entity-id-key="entityIdKey"
-                   :entity-param-key="entityParamKey"
-                   :show-route-name="showRouteName"
-                   :show-close-button="false"
-                   :show-edit-button="false"
-                   :show-expand-button="false"
-                   :show-save-button="false"
-                   :show-reload-button="false"
-                   :redirect-after-edit="false"
-                   :after-load-input-data="afterLoadInputData" />
-    </q-card>
+    <q-tab-panels v-model="panel"
+                  animated
+                  class="panels">
+      <q-tab-panel name="userInfo">
+        <q-banner class="banner complete-info-form-banner">
+          تکمیل اطلاعات
+        </q-banner>
+        <q-card class="complete-info-form-card">
+          <entity-edit v-if="mounted"
+                       ref="entityEdit"
+                       v-model:value="inputs"
+                       title="اطلاعات کاربری"
+                       :api="api"
+                       :entity-id-key="entityIdKey"
+                       :entity-param-key="entityParamKey"
+                       :show-route-name="showRouteName"
+                       :show-close-button="false"
+                       :show-edit-button="false"
+                       :show-expand-button="false"
+                       :show-save-button="false"
+                       :show-reload-button="false"
+                       :redirect-after-edit="false"
+                       :after-load-input-data="afterLoadInputData" />
+        </q-card>
 
-    <div class="flex justify-end section-edit-btn">
-      <q-btn color="primary"
-             @click="editEntity">
-        تایید و ادامه
-        <svg xmlns="http://www.w3.org/2000/svg"
-             width="18.387"
-             height="11.502"
-             viewBox="0 0 18.387 11.502">
-          <path id="Combined_Shape"
-                data-name="Combined Shape"
-                d="M-9.338,11.408a.748.748,0,0,0,.388-.656V6.5h8.2A.751.751,0,0,0,0,5.75.75.75,0,0,0-.75,5h-8.2V.75A.751.751,0,0,0-9.338.093.769.769,0,0,0-9.7,0a.735.735,0,0,0-.4.115l-7.938,5a.747.747,0,0,0-.35.635.742.742,0,0,0,.35.634l7.938,5a.751.751,0,0,0,.4.116A.746.746,0,0,0-9.338,11.408ZM-10.45,9.392l-5.78-3.641,5.78-3.642Z"
-                transform="translate(18.387)"
-                fill="#fff" />
-        </svg>
-      </q-btn>
-    </div>
+        <div class="flex justify-end section-edit-btn">
+          <q-btn color="primary"
+                 @click="editEntity">
+            تایید و ادامه
+            <svg xmlns="http://www.w3.org/2000/svg"
+                 width="18.387"
+                 height="11.502"
+                 viewBox="0 0 18.387 11.502">
+              <path id="Combined_Shape"
+                    data-name="Combined Shape"
+                    d="M-9.338,11.408a.748.748,0,0,0,.388-.656V6.5h8.2A.751.751,0,0,0,0,5.75.75.75,0,0,0-.75,5h-8.2V.75A.751.751,0,0,0-9.338.093.769.769,0,0,0-9.7,0a.735.735,0,0,0-.4.115l-7.938,5a.747.747,0,0,0-.35.635.742.742,0,0,0,.35.634l7.938,5a.751.751,0,0,0,.4.116A.746.746,0,0,0-9.338,11.408ZM-10.45,9.392l-5.78-3.641,5.78-3.642Z"
+                    transform="translate(18.387)"
+                    fill="#fff" />
+            </svg>
+          </q-btn>
+        </div>
+      </q-tab-panel>
+
+      <q-tab-panel name="eventTest">
+        <q-banner class="banner complete-info-form-banner">
+          پرسشنامه
+        </q-banner>
+        <show-event-test v-if="!eventTestAnswerBook.loading"
+                         :answer-book-id="eventTestAnswerBook.id" />
+        <div v-else>
+          کمی صبر کنید...
+        </div>
+      </q-tab-panel>
+
+    </q-tab-panels>
 
   </div>
 </template>
@@ -125,22 +143,28 @@
 import { EntityEdit } from 'quasar-crud'
 import Enums from 'src/assets/Enums/Enums.js'
 import { Invoice } from 'src/models/Invoice.js'
+import { mixinAuth } from 'src/mixin/Mixins.js'
 import ShamsiDate from 'src/assets/ShamsiDate.js'
 import { APIGateway } from 'src/api/APIGateway.js'
 import { Classroom } from 'src/models/Classroom.js'
+import { AnswerBook } from 'src/models/AnswerBook.js'
 import { FormBuilderAssist } from 'quasar-form-builder'
+import ShowEventTest from './components/ShowEventTest.vue'
 
 export default {
   name: 'ShopCompleteInfo',
-  components: { EntityEdit },
+  components: { EntityEdit, ShowEventTest },
+  mixins: [mixinAuth],
   data: () => ({
     mounted: false,
+    panel: 'userInfo',
     step: false,
     rulesAccept: false,
     rulesDialog: false,
     loading: true,
     classroom: new Classroom(),
     invoice: new Invoice(),
+    eventTestAnswerBook: new AnswerBook(),
 
     api: APIGateway.user.APIAdresses.current,
     entityIdKey: 'id',
@@ -238,22 +262,64 @@ export default {
   },
   mounted () {
     this.loadClassroomFromStore()
+    if (this.$route.query.source === 'event') {
+      this.getEventTest()
+    }
     this.mounted = true
+
+    this.$bus.on('event-confirmation-step-test-sent-answer-sending', () => {
+    })
+    this.$bus.on('event-confirmation-step-test-sent-answer-success', () => {
+      this.successSentTest()
+    })
+    this.$bus.on('event-confirmation-step-test-sent-answer-failed', () => {
+      this.sentTestFailed()
+    })
   },
   methods: {
+    getEventTest () {
+      if (this.classroom.tests && this.classroom.tests.length === 0) {
+        return
+      }
+      this.getEventTestAnswerBook(this.classroom.tests[0])
+    },
+    getEventTestAnswerBook (eventTestId) {
+      this.eventTestAnswerBook.loading = true
+      APIGateway.answerBook.getOrCreate(this.user.id, eventTestId)
+        .then((answerBook) => {
+          this.eventTestAnswerBook = new AnswerBook(answerBook)
+          this.eventTestAnswerBook.loading = false
+        })
+        .catch(() => {
+          this.eventTestAnswerBook.loading = false
+        })
+    },
     loadClassroomFromStore () {
       this.classroom = this.$store.getters['Shop/onRegisterClassroom']
     },
+    sentTestFailed () {
+      this.panel = 'userInfo'
+    },
+    successSentTest () {
+      this.editEntity()
+    },
     editEntity () {
-      this.$refs.entityEdit.editEntity(false)
-        .then(() => {
-          if (this.$route.query.type === 'register') {
-            this.createInvoiceForClassroomOrRegisterEvent()
-          } else {
-            this.$router.push({ name: 'UserPanel.ShopPaymentFromWallet', query: this.$route.query })
-          }
-        })
-        .catch(() => {})
+      if (this.eventTestAnswerBook.id && this.panel !== 'eventTest') {
+        this.panel = 'eventTest'
+        return
+      }
+      this.panel = 'userInfo'
+      this.$nextTick(() => {
+        this.$refs.entityEdit.editEntity(false)
+          .then(() => {
+            if (this.$route.query.type === 'register') {
+              this.createInvoiceForClassroomOrRegisterEvent()
+            } else {
+              this.$router.push({ name: 'UserPanel.ShopPaymentFromWallet', query: this.$route.query })
+            }
+          })
+          .catch(() => {})
+      })
     },
     createInvoiceForClassroomOrRegisterEvent () {
       this.register()
@@ -421,6 +487,9 @@ export default {
       }
     }
   }
+  :deep(.panels) {
+    background: transparent;
+  }
   .section-edit-btn {
     margin-top: 50px;
     margin-bottom: 77px;

+ 258 - 0
src/components/Widgets/Other/Shop/ShopCompleteInfo/components/AllQuestions.vue

@@ -0,0 +1,258 @@
+<template>
+  <div class="AllQuestions">
+    <div class="question-navigator">
+      <q-btn v-if="questionPanel < answerBook.answer_sheet_info.list.length"
+             color="indigo-7"
+             outline
+             size="sm"
+             class="btn-next-question"
+             @click="nextQuestion">
+        سوال بعدی
+        <q-icon name="arrow_back_ios" />
+      </q-btn>
+      <q-btn v-if="questionPanel > 1"
+             color="indigo-7"
+             outline
+             size="sm"
+             class="btn-prev-question"
+             @click="prevQuestion">
+        <q-icon name="arrow_forward_ios" />
+        سوال قبلی
+      </q-btn>
+      <q-linear-progress size="32px"
+                         :value="(questionPanel / answerBook.answer_sheet_info.list.length)"
+                         color="light-blue-5">
+        <div class="absolute-full flex flex-center">
+          <q-badge color="white"
+                   text-color="accent">
+            سوال
+            {{ questionPanel }}
+            از
+            {{ answerBook.answer_sheet_info.list.length }}
+          </q-badge>
+        </div>
+      </q-linear-progress>
+    </div>
+    <q-tab-panels v-model="questionPanel"
+                  animated>
+      <q-tab-panel v-for="(answerSheet, answerSheetIndex) in answerBook.answer_sheet_info.list"
+                   :key="answerSheetIndex"
+                   :name="(answerSheetIndex + 1)">
+        <div v-html="'<span>' + (answerSheetIndex + 1) + ' - </span>' + answerSheet.test_question_info.question_info.text" />
+        <div>
+          <entity-create v-if="mounted"
+                         :ref="'entityCreate_' + answerSheetIndex.toString()"
+                         v-model:value="questionInputs[answerSheetIndex]"
+                         :api="singleQuestionSendAnswerApi"
+                         :default-layout="false"
+                         :show-close-button="false"
+                         :show-edit-button="false"
+                         :show-expand-button="false"
+                         :show-save-button="false"
+                         :show-reload-button="false" />
+        </div>
+      </q-tab-panel>
+    </q-tab-panels>
+    <q-separator class="q-my-lg" />
+    <!--    <q-card-actions class="flex justify-end AllQuestionsForm">-->
+    <!--      <q-btn color="primary"-->
+    <!--             @click="confirmAnswers">-->
+    <!--        ارسال پرسشنامه-->
+    <!--      </q-btn>-->
+    <!--    </q-card-actions>-->
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityCreate } from 'quasar-crud'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { AnswerBook } from 'src/models/AnswerBook.js'
+import BtnControl from 'src/components/Control/btn.vue'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import QuestionChoices from 'src/components/FormBuilderCustumComponents/QuestionChoices/QuestionChoices.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const QuestionChoicesComp = shallowRef(QuestionChoices)
+
+export default {
+  name: 'AllQuestions',
+  components: { EntityCreate },
+  props: {
+    answerBook: {
+      type: AnswerBook,
+      default: new AnswerBook()
+    }
+  },
+  data () {
+    return {
+      mounted: false,
+      questionPanel: 1,
+      questionInputs: [],
+      singleQuestionSendAnswerApi: APIGateway.answerSheet.APIAdresses.base,
+      api: APIGateway.answerBook.APIAdresses.submitOverallAnswer(this.$route.params.answer_book_id)
+    }
+  },
+  mounted () {
+    this.setInputs()
+    this.mounted = true
+  },
+  methods: {
+    nextQuestion () {
+      this.sendAnswer(this.questionPanel - 1)
+        .then(() => {
+          this.changeQuestion(this.questionPanel + 1)
+        })
+        .catch(() => {})
+    },
+    prevQuestion () {
+      this.sendAnswer(this.questionPanel - 1)
+        .then(() => {
+          this.changeQuestion(this.questionPanel - 1)
+        })
+        .catch(() => {})
+    },
+    changeQuestion (newNumber) {
+      this.questionPanel = newNumber
+    },
+    setInputs () {
+      this.answerBook.answer_sheet_info.list.forEach((answerSheetItem, answerSheetIndex) => {
+        const hasChoise = Array.isArray(answerSheetItem.test_question_info.question_info.choices) && answerSheetItem.test_question_info.question_info.choices.length > 0
+        const inputs = [
+          { type: 'input', name: 'answer_text', label: 'متن پاسخ', placeholder: ' ', value: answerSheetItem.answer_text, inputType: 'textarea', col: 'col-12' },
+          { type: 'file', name: 'answer_attachment', label: 'فایل پیوست', placeholder: ' ', value: answerSheetItem.answer_attachment, col: 'col-12' },
+          { type: 'hidden', name: 'answer_book', value: this.answerBook.id },
+          { type: 'hidden', name: 'test_question', value: answerSheetItem.test_question },
+          {
+            type: BtnControlComp,
+            name: 'btn',
+            responseKey: 'btn',
+            label: 'ثبت پاسخ',
+            placeholder: ' ',
+            atClick: () => {
+              this.sendAnswer(answerSheetIndex)
+              // const answerText = FormBuilderAssist.getInputsByName(this.questionInputs[answerSheetIndex], 'answer_text').value
+              // if (!answerText && !hasChoise) {
+              //   this.$q.notify({
+              //     type: 'negative',
+              //     message: 'لطفا متن پاسخ را کامل کنید.'
+              //   })
+              //   return
+              // }
+              // const entityCreate = this.$refs['entityCreate_' + answerSheetIndex.toString()][0]
+              // entityCreate.entityLoading = true
+              // entityCreate.createEntity(false)
+              //   .then(() => {
+              //     if (this.questionPanel < this.answerBook.answer_sheet_info.list.length) {
+              //       this.changeQuestion(this.questionPanel + 1)
+              //     } else if (this.questionPanel === this.answerBook.answer_sheet_info.list.length) {
+              //       this.confirmAnswers()
+              //     }
+              //     entityCreate.entityLoading = false
+              //   })
+              //   .catch(() => {
+              //     entityCreate.entityLoading = false
+              //   })
+            },
+            col: 'col-12 flex justify-end'
+          }
+        ]
+
+        if (hasChoise) {
+          inputs[0].type = 'hidden'
+          inputs[0].value = null
+          inputs[1].type = 'hidden'
+          inputs[1].value = null
+          inputs.splice(2, 0,
+            {
+              type: QuestionChoicesComp,
+              name: 'answer_choice_index',
+              participateMode: true,
+              canAddChoice: false,
+              canEdit: false,
+              canDelete: false,
+              choicesInfo: answerSheetItem.test_question_info.question_info.choices_info,
+              value: isNaN(answerSheetItem.answer_choice_index) ? 0 : parseInt(answerSheetItem.answer_choice_index),
+              col: 'col-12 flex justify-end'
+            })
+        }
+
+        this.questionInputs.push(inputs)
+      })
+    },
+    sendAnswer (answerSheetIndex) {
+      return new Promise((resolve, reject) => {
+        const answerSheetItem = this.answerBook.answer_sheet_info.list[answerSheetIndex]
+        const hasChoise = Array.isArray(answerSheetItem.test_question_info.question_info.choices) && answerSheetItem.test_question_info.question_info.choices.length > 0
+        const answerText = FormBuilderAssist.getInputsByName(this.questionInputs[answerSheetIndex], 'answer_text').value
+        if (!answerText && !hasChoise) {
+          this.$q.notify({
+            type: 'negative',
+            message: 'لطفا متن پاسخ را کامل کنید.'
+          })
+          reject()
+          return
+        }
+        if (hasChoise && isNaN(answerSheetItem.answer_choice_index)) {
+          this.$q.notify({
+            type: 'negative',
+            message: 'لطفا یک گزینه را انتخاب.'
+          })
+          reject()
+          return
+        }
+        const entityCreate = this.$refs['entityCreate_' + answerSheetIndex.toString()][0]
+        entityCreate.entityLoading = true
+        entityCreate.createEntity(false)
+          .then(() => {
+            if (this.questionPanel < this.answerBook.answer_sheet_info.list.length) {
+              // this.changeQuestion(this.questionPanel + 1)
+            } else if (this.questionPanel === this.answerBook.answer_sheet_info.list.length) {
+              this.confirmAnswers()
+            }
+            entityCreate.entityLoading = false
+            resolve()
+          })
+          .catch(() => {
+            entityCreate.entityLoading = false
+            reject()
+          })
+      })
+    },
+    confirmAnswers () {
+      this.$bus.emit('event-confirmation-step-test-sent-answer-sending')
+      APIGateway.answerBook.confirmAnswers(this.answerBook.id)
+        .then(() => {
+          this.$bus.emit('event-confirmation-step-test-sent-answer-success')
+        })
+        .catch(() => {
+          this.$bus.emit('event-confirmation-step-test-sent-answer-failed')
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AllQuestions {
+  .question-navigator {
+    position: relative;
+    :deep(.q-btn) {
+      position: absolute;
+      top: 0;
+      z-index: 2;
+      &.btn-next-question {
+        right: 0;
+      }
+      &.btn-prev-question {
+        left: 0;
+      }
+    }
+  }
+  .AllQuestionsForm {
+    & > div {
+      width: 100%;
+    }
+  }
+}
+</style>

+ 118 - 0
src/components/Widgets/Other/Shop/ShopCompleteInfo/components/ShowEventTest.vue

@@ -0,0 +1,118 @@
+<template>
+  <div class="ShowEventTest">
+    <q-card v-if="!localAnswerBook.loading && localAnswerBook.id">
+      <q-card-section class="flex justify-between">
+        <div>{{ localAnswerBook.test_info.title }}</div>
+      </q-card-section>
+      <q-separator />
+      <template v-if="mounted">
+        <all-questions :answer-book="localAnswerBook" />
+      </template>
+    </q-card>
+    <div v-else>
+      کمی صبر کنید...
+    </div>
+  </div>
+</template>
+
+<script>
+import AllQuestions from './AllQuestions.vue'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { AnswerBook } from 'src/models/AnswerBook.js'
+
+export default {
+  name: 'ShowEventTest',
+  components: { AllQuestions },
+  props: {
+    answerBookId: {
+      type: Number,
+      default: null
+    }
+  },
+  data () {
+    return {
+      mounted: false,
+      participateType: null,
+      localAnswerBook: new AnswerBook(),
+      participateTypeDialog: false,
+      storeAnswerBook: null,
+      remainingTime: 0,
+      timerInterval: null
+    }
+  },
+  mounted () {
+    this.getTestQuestions()
+    this.mounted = true
+    this.$bus.on('event-confirmation-step-test-sent-answer-sending', () => {
+      this.onAllQuestionsSending()
+    })
+    this.$bus.on('event-confirmation-step-test-sent-answer-success', () => {
+      this.onAllQuestionsSentSuccess()
+    })
+    this.$bus.on('event-confirmation-step-test-sent-answer-failed', () => {
+      this.onAllQuestionsSentFailed()
+    })
+  },
+  methods: {
+    onAllQuestionsSending () {
+      this.localAnswerBook.loading = true
+    },
+    onAllQuestionsSentSuccess () {
+      this.localAnswerBook.loading = false
+    },
+    onAllQuestionsSentFailed () {
+      this.localAnswerBook.loading = false
+    },
+    backToClassList () {
+      this.$bus.emit('event-confirmation-step-get-test-failed')
+    },
+    getAnswerBook () {
+      this.localAnswerBook.loading = true
+      APIGateway.answerBook.get(this.answerBookId)
+        .then((answerBook) => {
+          this.localAnswerBook = new AnswerBook(answerBook)
+          if (this.localAnswerBook.test_info.test_questions_length === 0) {
+            this.$q.notify({
+              type: 'negative',
+              message: 'لیست سوالات پرسشنامه خالی است.'
+            })
+            this.backToClassList()
+          }
+          this.localAnswerBook.loading = false
+        })
+        .catch(() => {
+          this.localAnswerBook.loading = false
+          this.backToClassList()
+        })
+    },
+    initPageFromAnswerBook (answerBook) {
+      this.localAnswerBook = new AnswerBook(answerBook)
+      this.localAnswerBook.loading = false
+    },
+    getTestQuestions () {
+      this.localAnswerBook.loading = true
+      APIGateway.answerBook.getTestQuestions(this.answerBookId)
+        .then((answerBook) => {
+          this.initPageFromAnswerBook(answerBook)
+        })
+        .catch(() => {
+          this.localAnswerBook.loading = false
+          this.backToClassList()
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.ShowEventTest {
+  :deep(.q-banner) {
+    background: #FCF9F4 0 0 no-repeat padding-box;
+    padding: 20px;
+    .q-banner__content {
+      font-size: 16px;
+      font-weight: normal;
+    }
+  }
+}
+</style>

+ 1 - 1
src/components/Widgets/Other/ShowClassroomInfo/ShowClassroomInfo.vue

@@ -356,7 +356,7 @@ export default {
     },
     acceptAndContinue () {
       this.$store.commit('Shop/updateOnRegisterClassroom', this.classroom)
-      this.$router.push({ name: 'UserPanel.ShopCompleteInfo', query: { type: this.acceptClassType } })
+      this.$router.push({ name: 'UserPanel.ShopCompleteInfo', query: { type: this.acceptClassType, source: this.classroomType === 'EVENT' ? 'event' : 'classroom' } })
     },
     onEnrolment () {
       this.acceptClassType = 'enrollment'

+ 121 - 0
src/components/Widgets/SessionTemplate/AdminEventQuestionCreate/AdminEventQuestionCreate.vue

@@ -0,0 +1,121 @@
+<template>
+  <div class="AdminEventQuestionCreate"
+       :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="entityEdit"
+                   v-model:value="inputs"
+                   title="ایجاد سوال جدید"
+                   :api="api"
+                   :entity-id-key="entityIdKey"
+                   :entity-param-key="entityParamKey"
+                   :show-route-name="showRouteName"
+                   :show-close-button="false"
+                   :show-edit-button="false"
+                   :show-expand-button="false"
+                   :show-save-button="false"
+                   :show-reload-button="false"
+                   :redirect-after-edit="false"
+                   :after-load-input-data="afterLoadInputData" />
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityCreate } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { Question } from 'src/models/Question.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import BtnControl from 'src/components/Control/btn.vue'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import QuestionChoices from 'src/components/FormBuilderCustumComponents/QuestionChoices/QuestionChoices.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const QuestionChoicesComp = shallowRef(QuestionChoices)
+
+export default {
+  name: 'AdminEventQuestionCreate',
+  components: {
+    EntityCreate
+  },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      mounted: false,
+      entityLoading: true,
+      api: APIGateway.question.APIAdresses.base,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Event.TestSet.Questions.Show',
+      loadedData: new Question(),
+      inputs: [
+        { type: 'input', name: 'text', responseKey: 'text', label: 'سوال', placeholder: ' ', col: 'col-12' },
+        { type: 'inputEditor', name: 'correct_answer', responseKey: 'correct_answer', label: 'پاسخ', placeholder: ' ', col: 'col-12' },
+        { type: QuestionChoicesComp, name: 'choices', placeholder: ' ', col: 'col-12 flex justify-end' },
+        { type: BtnControlComp, name: 'btn', label: 'ذخیره سوال', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' },
+        { type: 'hidden', name: 'type', responseKey: 'type', value: 'EVENT' }
+      ]
+    }
+  },
+  mounted() {
+    this.setActionBtn()
+    this.mounted = true
+  },
+  methods: {
+    setActionBtn () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.edit)
+    },
+    afterLoadInputData () {
+      this.entityLoading = false
+    },
+    edit() {
+      this.entityLoading = true
+      this.$refs.entityEdit.createEntity(false)
+        .then((response) => {
+          this.$router.push({ name: 'Admin.Event.TestSet.Questions.Show', params: { question_id: response.data.id } })
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminEventQuestionCreate {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    display: flex;
+    flex-flow: row;
+    position: relative;
+    .static-title {
+
+    }
+    .dynamic-title {
+
+    }
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/SessionTemplate/AdminEventQuestionCreate/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>

+ 160 - 0
src/components/Widgets/SessionTemplate/AdminEventQuestionList/AdminEventQuestionList.vue

@@ -0,0 +1,160 @@
+<template>
+  <div class="AdminEventQuestionList"
+       :style="localOptions.style">
+    <!--    <q-skeleton v-if="mounted"-->
+    <!--                type="text"-->
+    <!--                width="200px" />-->
+    <!--    <breadcrumbs v-else-->
+    <!--                 style="margin-top: 29px; margin-bottom: 19px;" />-->
+    <div class="flex justify-end q-mb-md">
+      <q-btn label="ایجاد سوال جدید"
+             color="primary"
+             class="q-mr-md"
+             :to="{ name: 'Admin.Unit.Questions.Create', pragma: { unit_id: $route.params.unit_id, session_template_id: $route.params.session_template_id } }" />
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-index v-if="mounted"
+                  v-model:value="inputs"
+                  title="لیست سوالات"
+                  :api="api"
+                  :table="table"
+                  :table-keys="tableKeys"
+                  :create-route-name="createRouteName"
+                  :show-search-button="false"
+                  :show-expand-button="false"
+                  :show-reload-button="false">
+      <template #entity-index-table-cell="{inputData, showConfirmRemoveDialog}">
+        <template v-if="inputData.col.name === 'number'">
+          {{ inputData.rowNumber }}
+        </template>
+        <template v-else-if="inputData.col.name === 'action'">
+          <div class="action-column-entity-index">
+            <q-btn color="primary"
+                   outline
+                   round
+                   icon="drive_file_rename_outline"
+                   :to="{ name: 'Admin.Unit.Questions.Show', params: { unit_id: this.$route.params.unit_id, session_template_id: this.$route.params.session_template_id, id: inputData.props.row.id } }"
+                   class="q-mr-md" />
+            <delete-btn @click="showConfirmRemoveDialog(inputData.props.row, 'id', getRemoveMessage(inputData.props.row))" />
+          </div>
+        </template>
+        <template v-else>
+          {{ inputData.col.value }}
+        </template>
+      </template>
+    </entity-index>
+  </div>
+</template>
+
+<script>
+import { EntityIndex } from 'quasar-crud'
+import { Question } from 'src/models/Question.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import DeleteBtn from 'src/components/Control/DeleteBtn.vue'
+// import Breadcrumbs from 'src/components/Widgets/Breadcrumbs/Breadcrumbs.vue'
+
+export default {
+  name: 'AdminEventQuestionList',
+  components: {
+    EntityIndex,
+    // Breadcrumbs,
+    DeleteBtn
+  },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      api: APIGateway.question.APIAdresses.base,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [
+        { type: 'hidden', name: 'type', value: 'QUESTION_BANK' },
+        { type: 'hidden', name: 'session_template_questions__session_template', value: this.$route.params.session_template_id }
+      ],
+      table: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'id',
+            required: true,
+            label: 'شناسه',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'text',
+            required: true,
+            label: 'سوال',
+            align: 'left',
+            field: row => row.text
+          },
+          {
+            name: 'mark',
+            required: true,
+            label: 'بارم',
+            align: 'left',
+            field: row => row.mark
+          },
+          {
+            name: 'level',
+            required: true,
+            label: 'سطح سوال',
+            align: 'left',
+            field: row => new Question(row).level_info.label
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'عملیات',
+            align: 'left',
+            field: row => ''
+          }
+        ]
+      },
+      mounted: false,
+      createRouteName: ''
+    }
+  },
+  mounted() {
+    this.mounted = true
+    // this.updateBreadcrumbs()
+  },
+  methods: {
+    updateBreadcrumbs () {
+      this.$store.commit('AppLayout/updateBreadcrumbs', {
+        visible: true,
+        loading: false,
+        path: [
+          // {
+          //   label: 'لیست ',
+          //   to: { name: this.indexPageRouteName }
+          // },
+          // {
+          //   label: 'this.classroom.title',
+          //   to: { name: this.showPageRouteName, params: { id: 12 } }
+          // }
+        ]
+      })
+    },
+    getRemoveMessage (row) {
+      return 'آیا از حذف (' + row.text + ') اطمینان دارید؟'
+    }
+  }
+}
+</script>

+ 21 - 0
src/components/Widgets/SessionTemplate/AdminEventQuestionList/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>

+ 125 - 0
src/components/Widgets/SessionTemplate/AdminEventQuestionShow/AdminEventQuestionShow.vue

@@ -0,0 +1,125 @@
+<template>
+  <div class="AdminEventQuestionShow"
+       :style="localOptions.style">
+    <div class="flex justify-end">
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-edit v-if="mounted"
+                 ref="entityEdit"
+                 v-model:value="inputs"
+                 title="ایجاد سوال جدید"
+                 :api="api"
+                 :entity-id-key="entityIdKey"
+                 :entity-param-key="entityParamKey"
+                 :show-route-name="showRouteName"
+                 :show-close-button="false"
+                 :show-edit-button="false"
+                 :show-expand-button="false"
+                 :show-save-button="false"
+                 :show-reload-button="false"
+                 :redirect-after-edit="false"
+                 :before-load-input-data="beforeLoadInputData" />
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityEdit } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import BtnControl from 'src/components/Control/btn.vue'
+import QuestionChoices from 'src/components/FormBuilderCustumComponents/QuestionChoices/QuestionChoices.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const QuestionChoicesComp = shallowRef(QuestionChoices)
+
+export default {
+  name: 'AdminEventQuestionShow',
+  components: {
+    EntityEdit
+  },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      mounted: false,
+      entityLoading: true,
+      api: APIGateway.question.APIAdresses.byId(this.$route.params.question_id),
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Event.TestSet.Questions.Show',
+      inputs: [
+        { type: 'input', name: 'text', responseKey: 'text', label: 'سوال', placeholder: ' ', col: 'col-12' },
+        { type: 'inputEditor', name: 'correct_answer', responseKey: 'correct_answer', label: 'پاسخ', placeholder: ' ', col: 'col-12' },
+        { type: QuestionChoicesComp, name: 'choices', responseKey: 'choices_info', correctChoiceIndex: 0, col: 'col-12 flex justify-end' },
+        { type: BtnControlComp, name: 'btn', label: 'ویرایش سوال', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' },
+        { type: 'hidden', name: 'correct_choice_index', responseKey: 'correct_choice_index' },
+        { type: 'hidden', name: 'type', responseKey: 'type', value: 'EVENT' }
+      ]
+    }
+  },
+  mounted() {
+    this.setActionBtn()
+    this.mounted = true
+    this.$bus.on('onChangeCorrectChoiceIndexInQuestionChoicesInput', (correctChoiceIndex) => {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'correct_choice_index', 'value', correctChoiceIndex)
+    })
+  },
+  methods: {
+    setActionBtn () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.edit)
+    },
+    beforeLoadInputData (responseData) {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'choices', 'correctChoiceIndex', responseData.correct_choice_index)
+      this.entityLoading = false
+    },
+    edit() {
+      this.entityLoading = true
+      this.$refs.entityEdit.editEntity(false)
+        .then(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminEventQuestionShow {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    display: flex;
+    flex-flow: row;
+    position: relative;
+    .static-title {
+
+    }
+    .dynamic-title {
+
+    }
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/SessionTemplate/AdminEventQuestionShow/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>

+ 121 - 0
src/components/Widgets/SessionTemplate/AdminSessionTemplateQuestionCreate/AdminSessionTemplateQuestionCreate.vue

@@ -0,0 +1,121 @@
+<template>
+  <div class="AdminSessionTemplateQuestionCreate"
+       :style="localOptions.style">
+    <div class="flex justify-end">
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-edit v-if="mounted"
+                 ref="entityEdit"
+                 v-model:value="inputs"
+                 title="ایجاد سوال جدید"
+                 :api="api"
+                 :loaded-data="loadedData"
+                 :entity-id-key="entityIdKey"
+                 :entity-param-key="entityParamKey"
+                 :show-route-name="showRouteName"
+                 :show-close-button="false"
+                 :show-edit-button="false"
+                 :show-expand-button="false"
+                 :show-save-button="false"
+                 :show-reload-button="false"
+                 :redirect-after-edit="false"
+                 :after-load-input-data="afterLoadInputData" />
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityEdit } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { Question } from 'src/models/Question.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import BtnControl from 'src/components/Control/btn.vue'
+import { FormBuilderAssist } from 'quasar-form-builder'
+
+const BtnControlComp = shallowRef(BtnControl)
+
+export default {
+  name: 'AdminSessionTemplateQuestionCreate',
+  components: {
+    EntityEdit
+  },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      mounted: false,
+      entityLoading: true,
+      api: APIGateway.sessionTemplate.APIAdresses.appendQuestion(this.$route.params.session_template_id),
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Unit.Questions.Show',
+      loadedData: new Question(),
+      inputs: [
+        { type: 'input', name: 'text', responseKey: 'text', label: 'سوال', placeholder: ' ', col: 'col-12' },
+        { type: 'inputEditor', name: 'correct_answer', responseKey: 'correct_answer', label: 'پاسخ', placeholder: ' ', col: 'col-12' },
+        { type: 'input', name: 'mark', responseKey: 'mark', label: 'بارم پیشنهادی سوال', placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'select', name: 'level', responseKey: 'level', label: 'سطح سوال', placeholder: ' ', options: (new Question()).levelEnums, col: 'col-md-3 col-12' },
+        { type: BtnControlComp, name: 'btn', label: 'ذخیره سوال', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' },
+        { type: 'hidden', name: 'type', responseKey: 'type', value: 'QUESTION_BANK' }
+      ]
+    }
+  },
+  mounted() {
+    this.setActionBtn()
+    this.mounted = true
+  },
+  methods: {
+    setActionBtn () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.edit)
+    },
+    afterLoadInputData () {
+      this.entityLoading = false
+    },
+    edit() {
+      this.entityLoading = true
+      this.$refs.entityEdit.editEntity(false)
+        .then((response) => {
+          this.$router.push({ name: 'Admin.Unit.Questions.Show', params: { unit_id: this.$route.params.unit_id, session_template_id: this.$route.params.session_template_id, id: response.data.id } })
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminSessionTemplateQuestionCreate {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    display: flex;
+    flex-flow: row;
+    position: relative;
+    .static-title {
+
+    }
+    .dynamic-title {
+
+    }
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/SessionTemplate/AdminSessionTemplateQuestionCreate/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>

+ 213 - 0
src/components/Widgets/SessionTemplate/AdminSessionTemplateQuestionList/AdminSessionTemplateQuestionList.vue

@@ -0,0 +1,213 @@
+<template>
+  <div class="AdminSessionTemplateQuestionList"
+       :style="localOptions.style">
+    <q-skeleton v-if="!unit.id || !sessionTemplate.id"
+                type="text"
+                width="200px" />
+    <breadcrumbs v-else
+                 style="margin-top: 29px; margin-bottom: 19px;" />
+    <div class="flex justify-end q-mb-md">
+      <q-btn label="ایجاد سوال جدید"
+             color="primary"
+             class="q-mr-md"
+             :to="{ name: 'Admin.Unit.Questions.Create', pragma: { unit_id: $route.params.unit_id, session_template_id: $route.params.session_template_id } }" />
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-index v-if="mounted"
+                  v-model:value="inputs"
+                  title="لیست سوالات"
+                  :api="api"
+                  :table="table"
+                  :table-keys="tableKeys"
+                  :create-route-name="createRouteName"
+                  :show-search-button="false"
+                  :show-expand-button="false"
+                  :show-reload-button="false">
+      <template #entity-index-table-cell="{inputData, showConfirmRemoveDialog}">
+        <template v-if="inputData.col.name === 'number'">
+          {{ inputData.rowNumber }}
+        </template>
+        <template v-else-if="inputData.col.name === 'action'">
+          <div class="action-column-entity-index">
+            <q-btn color="primary"
+                   outline
+                   round
+                   icon="drive_file_rename_outline"
+                   :to="{ name: 'Admin.Unit.Questions.Show', params: { unit_id: this.$route.params.unit_id, session_template_id: this.$route.params.session_template_id, id: inputData.props.row.id } }"
+                   class="q-mr-md" />
+            <delete-btn @click="showConfirmRemoveDialog(inputData.props.row, 'id', getRemoveMessage(inputData.props.row))" />
+          </div>
+        </template>
+        <template v-else>
+          {{ inputData.col.value }}
+        </template>
+      </template>
+    </entity-index>
+  </div>
+</template>
+
+<script>
+import { EntityIndex } from 'quasar-crud'
+import { Unit } from 'src/models/Unit.js'
+import { Question } from 'src/models/Question.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import DeleteBtn from 'src/components/Control/DeleteBtn.vue'
+import { SessionTemplate } from 'src/models/SessionTemplate.js'
+import Breadcrumbs from 'src/components/Widgets/Breadcrumbs/Breadcrumbs.vue'
+
+export default {
+  name: 'AdminSessionTemplateQuestionList',
+  components: {
+    DeleteBtn,
+    Breadcrumbs,
+    EntityIndex
+  },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      unit: new Unit(),
+      sessionTemplate: new SessionTemplate(),
+      api: APIGateway.question.APIAdresses.base,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [
+        { type: 'hidden', name: 'type', value: 'QUESTION_BANK' },
+        { type: 'hidden', name: 'session_template_questions__session_template', value: this.$route.params.session_template_id }
+      ],
+      table: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'id',
+            required: true,
+            label: 'شناسه',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'text',
+            required: true,
+            label: 'سوال',
+            align: 'left',
+            field: row => row.text
+          },
+          {
+            name: 'mark',
+            required: true,
+            label: 'بارم',
+            align: 'left',
+            field: row => row.mark
+          },
+          {
+            name: 'level',
+            required: true,
+            label: 'سطح سوال',
+            align: 'left',
+            field: row => new Question(row).level_info.label
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'عملیات',
+            align: 'left',
+            field: row => ''
+          }
+        ]
+      },
+      mounted: false,
+      createRouteName: ''
+    }
+  },
+  mounted() {
+    this.mounted = true
+    Promise.all([
+      this.getUnit(),
+      this.getSessionTemplate()
+    ])
+      .then(() => {
+        this.updateBreadcrumbs()
+      })
+      .catch(() => {
+      })
+  },
+  methods: {
+    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()
+          })
+      })
+    },
+    getSessionTemplate () {
+      return new Promise((resolve, reject) => {
+        this.sessionTemplate.loading = true
+        APIGateway.sessionTemplate.get(this.$route.params.session_template_id)
+          .then((sessionTemplate) => {
+            this.sessionTemplate = new SessionTemplate(sessionTemplate)
+            this.sessionTemplate.loading = false
+            resolve()
+          })
+          .catch(() => {
+            this.sessionTemplate.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.sessionTemplate.title,
+            to: { name: 'Admin.SessionTemplate.Show', params: { id: this.sessionTemplate.id } }
+          },
+          {
+            label: 'لیست سوالات'
+          }
+        ]
+      })
+    },
+    getRemoveMessage (row) {
+      return 'آیا از حذف (' + row.text + ') اطمینان دارید؟'
+    }
+  }
+}
+</script>

+ 21 - 0
src/components/Widgets/SessionTemplate/AdminSessionTemplateQuestionList/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>

+ 120 - 0
src/components/Widgets/SessionTemplate/AdminSessionTemplateQuestionShow/AdminSessionTemplateQuestionShow.vue

@@ -0,0 +1,120 @@
+<template>
+  <div class="AdminSessionTemplateQuestionShow"
+       :style="localOptions.style">
+    <div class="flex justify-end">
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-edit v-if="mounted"
+                 ref="entityEdit"
+                 v-model:value="inputs"
+                 title="ایجاد سوال جدید"
+                 :api="api"
+                 :entity-id-key="entityIdKey"
+                 :entity-param-key="entityParamKey"
+                 :show-route-name="showRouteName"
+                 :show-close-button="false"
+                 :show-edit-button="false"
+                 :show-expand-button="false"
+                 :show-save-button="false"
+                 :show-reload-button="false"
+                 :redirect-after-edit="false"
+                 :after-load-input-data="afterLoadInputData" />
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityEdit } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { Question } from 'src/models/Question.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import BtnControl from 'src/components/Control/btn.vue'
+import { FormBuilderAssist } from 'quasar-form-builder'
+
+const BtnControlComp = shallowRef(BtnControl)
+
+export default {
+  name: 'AdminSessionTemplateQuestionShow',
+  components: {
+    EntityEdit
+  },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      mounted: false,
+      entityLoading: true,
+      api: APIGateway.question.APIAdresses.byId(this.$route.params.id),
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Unit.Questions.Show',
+      inputs: [
+        { type: 'input', name: 'text', responseKey: 'text', label: 'سوال', placeholder: ' ', col: 'col-12' },
+        { type: 'inputEditor', name: 'correct_answer', responseKey: 'correct_answer', label: 'پاسخ', placeholder: ' ', col: 'col-12' },
+        { type: 'input', name: 'mark', responseKey: 'mark', label: 'بارم پیشنهادی سوال', placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'select', name: 'level', responseKey: 'level', label: 'سطح سوال', placeholder: ' ', options: (new Question()).levelEnums, col: 'col-md-3 col-12' },
+        { type: BtnControlComp, name: 'btn', label: 'ویرایش سوال', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' },
+        { type: 'hidden', name: 'type', responseKey: 'type', value: 'QUESTION_BANK' }
+      ]
+    }
+  },
+  mounted() {
+    this.setActionBtn()
+    this.mounted = true
+  },
+  methods: {
+    setActionBtn () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.edit)
+    },
+    afterLoadInputData () {
+      this.entityLoading = false
+    },
+    edit() {
+      this.entityLoading = true
+      this.$refs.entityEdit.editEntity(false)
+        .then(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminSessionTemplateQuestionShow {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    display: flex;
+    flex-flow: row;
+    position: relative;
+    .static-title {
+
+    }
+    .dynamic-title {
+
+    }
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/SessionTemplate/AdminSessionTemplateQuestionShow/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>

+ 313 - 0
src/components/Widgets/Test/AdminAnswerBookShow/AdminAnswerBookShow.vue

@@ -0,0 +1,313 @@
+<template>
+  <div class="AdminAnswerBookShow"
+       :style="localOptions.style">
+    <q-card v-if="!answerBook.loading && answerBook.id">
+      <q-card-section class="flex justify-between">
+        <div>{{ answerBook.test_info.title }}</div>
+      </q-card-section>
+      <q-separator />
+      <q-banner>
+        <div class="row">
+          <div class="col-md-6 col-12">
+            وضعیت آزمون:
+            {{ answerBook.status_info.label }}
+          </div>
+          <div v-if="answerBook.grader_info"
+               class="col-md-6 col-12">
+            مصحح:
+            {{ answerBook.grader_info.firstname }}
+            {{ answerBook.grader_info.lastname }}
+            (کدملی: {{ answerBook.grader_info.national_code }})
+          </div>
+        </div>
+      </q-banner>
+      <q-separator />
+      <q-banner>
+        <div class="row">
+          <div class="col-md-4 col-12">
+            زمان شروع:
+            {{ toShamsi(answerBook.attending_start_time) }}
+          </div>
+          <div class="col-md-4 col-12">
+            {{ answerBook.owner_info.firstname }}
+            {{ answerBook.owner_info.lastname }}
+            (کدملی: {{ answerBook.owner_info.national_code }})
+          </div>
+          <div class="col-md-4 col-12">
+            نمره نهایی
+            <template v-if="answerBook.status === 'GRADING'">
+              {{ totalScore }}
+            </template>
+            <template v-else>
+              {{ answerBook.total_score }}
+            </template>
+          </div>
+        </div>
+      </q-banner>
+      <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 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 />
+                  </div>
+                </q-card-section>
+                <q-card-section v-else>
+                  <div>
+                    متن پاسخ:
+                  </div>
+                  <div>
+                    {{ answerSheet.answer_text }}
+                  </div>
+                  <div v-if="answerSheet.answer_attachment">
+                    <q-separator class="q-my-md" />
+                    <q-btn outline
+                           color="primary"
+                           icon="attachment"
+                           label="فایل پیوست"
+                           :href="answerSheet.answer_attachment"
+                           target="_blank" />
+                  </div>
+                </q-card-section>
+              </q-card>
+            </div>
+            <div v-if="$route.query.classroom_type !== 'EVENT'"
+                 class="col-md-2 col-12">
+              <q-badge v-if="answerSheet.score !== null">
+                {{ answerSheet.score }}
+                از
+                {{ answerSheet.test_question_info.question_info.mark }}
+                نمره
+              </q-badge>
+              <q-badge v-else>
+                {{ answerSheet.test_question_info.question_info.mark }}
+                نمره
+              </q-badge>
+              <div class="q-mt-md input-grate-question">
+                <q-input v-model="scores[answerSheetIndex].score"
+                         :loading="scores[answerSheetIndex].loading"
+                         dense>
+                  <template v-slot:append>
+                    <q-btn color="primary"
+                           label="ثبت نمره"
+                           outline
+                           :loading="scores[answerSheetIndex].loading"
+                           @click="submitScore(answerSheetIndex)" />
+                  </template>
+                </q-input>
+              </div>
+            </div>
+          </div>
+        </q-item>
+      </q-list>
+      <q-separator class="q-my-lg" />
+      <div class="q-pa-lg">
+        <entity-show v-if="mounted"
+                     ref="entityShow"
+                     v-model:value="overallAnswerInput"
+                     :loaded-data="answerBook"
+                     :show-close-button="false"
+                     :show-edit-button="false"
+                     :show-expand-button="false"
+                     :show-save-button="false"
+                     :show-index-button="false"
+                     :show-reload-button="false"
+                     :default-layout="false" />
+      </div>
+      <template v-if="answerBook.objection_request">
+        <div>
+          متن اعتراض:
+        </div>
+        <div v-html="answerBook.objection_request" />
+      </template>
+      <template v-if="answerBook.objection_request && !answerBook.objection_result">
+        <q-separator class="q-my-xl" />
+        <q-card-section>
+          <entity-edit v-if="mounted"
+                       ref="objectionEntityEdit"
+                       v-model:value="objectionInputs"
+                       title="پاسخ به اعتراض"
+                       :api="objectionApi"
+                       :loaded-data="{}"
+                       :show-close-button="false"
+                       :show-edit-button="false"
+                       :show-expand-button="false"
+                       :show-save-button="false"
+                       :show-reload-button="false">
+            <template #after-form-builder>
+              <div class="flex justify-end q-mt-md">
+                <q-btn color="primary"
+                       label="ثبت اعتراض"
+                       @click="sendObjection" />
+              </div>
+            </template>
+          </entity-edit>
+        </q-card-section>
+      </template>
+      <template v-else-if="answerBook.objection_result">
+        <div>
+          نتیجه اعتراض:
+        </div>
+        <div v-html="answerBook.objection_result" />
+      </template>
+    </q-card>
+    <div v-else>
+      کمی صبر کنید...
+    </div>
+  </div>
+</template>
+
+<script>
+import Assist from 'src/assets/js/Assist.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { EntityShow, EntityEdit } from 'quasar-crud'
+import { AnswerBook } from 'src/models/AnswerBook.js'
+import { mixinWidget, mixinAuth } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'AdminAnswerBookShow',
+  components: { EntityShow, EntityEdit },
+  mixins: [mixinWidget, mixinAuth],
+  data () {
+    return {
+      mounted: false,
+      overallAnswerInput: [
+        // { type: 'input', name: 'overall_answer_text', responseKey: 'overall_answer_text', label: 'متن پاسخ جامع', placeholder: ' ', inputType: 'textarea', col: 'col-12' },
+        { type: 'file', name: 'overall_answer_attachment', responseKey: 'overall_answer_attachment', label: 'فایل پیوست جامع', placeholder: ' ', col: 'col-12' }
+      ],
+      questionInput: [
+        { type: 'input', name: 'answer_text', responseKey: 'answer_text', label: 'متن پاسخ', placeholder: ' ', inputType: 'textarea', col: 'col-12' },
+        { type: 'file', name: 'answer_attachment', responseKey: 'answer_attachment', label: 'فایل پیوست', placeholder: ' ', col: 'col-12' }
+      ],
+      questionInputs: [],
+      scores: [],
+      objectionInputs: [
+        { type: 'input', name: 'objection_result', responseKey: 'objection_result', label: 'متن پاسخ', placeholder: ' ', inputType: 'textarea', col: 'col-12' }
+      ],
+      objectionApi: APIGateway.answerBook.APIAdresses.submitObjectionResult(this.$route.params.answer_book_id),
+      answerBook: new AnswerBook()
+    }
+  },
+  computed: {
+    totalScore () {
+      let totalScore = 0
+      this.scores.forEach(item => {
+        if (item.score) {
+          totalScore += parseFloat(item.score)
+        }
+      })
+      return totalScore
+    }
+  },
+  mounted () {
+    this.getAnswerBook()
+    this.mounted = true
+  },
+  methods: {
+    sendObjection () {
+      this.answerBook.loading = true
+      this.$refs.objectionEntityEdit.editEntity(false)
+        .then(() => {
+          this.getAnswerBook()
+        })
+        .catch(() => {
+          this.getAnswerBook()
+        })
+    },
+    toShamsi (miladi) {
+      return Assist.miladiToShamsi(miladi)
+    },
+    loadAnswerBook (answerBook) {
+      this.answerBook = new AnswerBook(answerBook)
+      this.answerBook.answer_sheet_info.list.forEach((answerSheetItem) => {
+        this.questionInputs.push(this.questionInput)
+        this.scores.push({
+          answerSheetId: answerSheetItem.id,
+          score: answerSheetItem.score,
+          loading: false
+        })
+      })
+    },
+    setGraderToMe () {
+      this.answerBook.loading = true
+      APIGateway.answerBook.setGraderToMe(this.$route.params.answer_book_id)
+        .then((answerBook) => {
+          this.loadAnswerBook(answerBook)
+          this.answerBook.loading = false
+        })
+        .catch(() => {
+          this.getAnswerBook()
+          this.answerBook.loading = false
+        })
+    },
+    submitScore (index) {
+      this.scores[index].loading = true
+      APIGateway.answerSheet.submitScore(this.scores[index].answerSheetId, this.scores[index].score)
+        .then(() => {
+          this.scores[index].loading = false
+          this.getAnswerBook()
+        })
+        .catch(() => {
+          this.scores[index].loading = false
+        })
+    },
+    getAnswerBook () {
+      this.answerBook.loading = true
+      APIGateway.answerBook.get(this.$route.params.answer_book_id)
+        .then((answerBook) => {
+          this.loadAnswerBook(answerBook)
+          this.answerBook.loading = false
+        })
+        .catch(() => {
+          this.answerBook.loading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminAnswerBookShow {
+  :deep(.q-banner) {
+    background: #FCF9F4 0 0 no-repeat padding-box;
+    padding: 20px;
+    .q-banner__content {
+      font-size: 16px;
+      font-weight: normal;
+    }
+  }
+  :deep(.q-list) {
+    .q-item {
+      .input-grate-question {
+        .q-input {
+          .q-field__inner .q-field__control {
+            height: 40px;
+            min-height: 40px;
+          }
+        }
+      }
+      .q-badge {
+        background: #ACBCAE 0 0 no-repeat padding-box;
+        border-radius: 18px;
+        height: 32px;
+        padding: 4px 16px;
+      }
+    }
+  }
+}
+</style>

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

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

@@ -0,0 +1,149 @@
+<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="$route.query.classroom_type === 'EVENT' ? 'مشخصات پرسشنامه' : 'مشخصات آزمون'"
+                   :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 { TestSet } from 'src/models/TestSet.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 UsersOfTestSelector from 'src/components/FormBuilderCustumComponents/UsersOfTestSelector/UsersOfTestSelector.vue'
+import { Test } from 'src/models/Test'
+
+const BtnControlComp = shallowRef(BtnControl)
+const UsersOfTestSelectorComp = shallowRef(UsersOfTestSelector)
+
+export default {
+  name: 'AdminTestCreate',
+  components: { EntityCreate },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      mounted: false,
+      testSet: new TestSet(),
+      entityLoading: false,
+      api: APIGateway.test.APIAdresses.base,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Unit.TestSet.Show',
+      classroomInputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات آزمون', col: 'col-12' },
+        { type: 'dateTime', name: 'start_time', responseKey: 'start_time', label: 'زمان شروع آزمون', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'dateTime', name: 'end_time', responseKey: 'end_time', label: 'زمان پایان آزمون', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'input', name: 'duration_deadline', responseKey: 'duration_deadline', label: 'مدت زمان پاسخ دهی (دقیقه)', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'dateTime', name: 'announcement_time', responseKey: 'announcement_time', label: 'زمان اعلام نتایج', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'dateTime', name: 'objection_start_time', responseKey: 'objection_start_time', label: 'زمان شروع اعتراض', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'dateTime', name: 'objection_end_time', responseKey: 'objection_end_time', label: 'زمان پایان اعتراض', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'select', name: 'level', responseKey: 'level', label: 'سطح آزمون', placeholder: ' ', options: (new Test()).levelEnums, col: 'col-md-3 col-12' },
+        { type: 'hidden', name: 'test_set', responseKey: 'test_set', value: this.$route.params.test_set_id },
+        { type: 'hidden', name: 'classroom', responseKey: 'classroom', value: this.$route.params.classroom_id },
+        { type: UsersOfTestSelectorComp, name: 'examinees', responseKey: 'examinees', classroomId: this.$route.params.classroom_id, col: 'col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'تعیین جزییات', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' }
+      ],
+      eventInputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات آزمون', col: 'col-12' },
+        { type: 'hidden', name: 'event', responseKey: 'event', value: this.$route.params.classroom_id },
+        { type: 'hidden', name: 'test_set', responseKey: 'test_set', value: this.$route.params.test_set_id },
+        { type: 'hidden', name: 'classroom', responseKey: 'classroom', value: this.$route.params.classroom_id },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'تعیین جزییات', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' }
+      ],
+      inputs: []
+    }
+  },
+  created() {
+    if (this.$route.query.classroom_type === 'EVENT') {
+      this.inputs = this.eventInputs
+    } else {
+      this.inputs = this.classroomInputs
+    }
+  },
+  mounted() {
+    this.setActionBtn()
+    this.getTestSet()
+    this.mounted = true
+  },
+  methods: {
+    getTestSet () {
+      this.testSet.loading = true
+      APIGateway.testSet.get(this.$route.params.test_set_id)
+        .then((testSet) => {
+          this.testSet.loading = false
+          this.testSet = new TestSet(testSet)
+          FormBuilderAssist.setAttributeByName(this.inputs, 'title', 'value', this.testSet.title)
+          FormBuilderAssist.setAttributeByName(this.inputs, 'description', 'value', this.testSet.description)
+        })
+        .catch(() => {
+          this.testSet.loading = false
+        })
+    },
+    setActionBtn () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.create)
+    },
+    create() {
+      this.entityLoading = true
+      this.$refs.entityCreate.createEntity(false)
+        .then((response) => {
+          this.$router.push({
+            name: 'Admin.Classroom.TestSet.Test.Show',
+            params: { classroom_id: this.$route.params.classroom_id, test_set_id: this.$route.params.test_set_id, id: response.data.id },
+            query: this.$route.query
+          })
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminTestCreate {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    position: relative;
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Test/AdminTestCreate/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 206 - 0
src/components/Widgets/Test/AdminTestShow/AdminTestShow.vue

@@ -0,0 +1,206 @@
+<template>
+  <div class="AdminTestShow"
+       :style="localOptions.style">
+    <q-skeleton v-if="!classroom.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 && classroom.id"
+                 ref="entityEdit"
+                 v-model:value="inputs"
+                 :title="$route.query.classroom_type === 'EVENT' ? 'مشخصات پرسشنامه' : 'مشخصات آزمون'"
+                 :api="api"
+                 :entity-id-key="entityIdKey"
+                 :entity-param-key="entityParamKey"
+                 :after-load-input-data="afterLoadInputData"
+                 :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 { Test } from 'src/models/Test.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { Classroom } from 'src/models/Classroom.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import BtnControl from 'src/components/Control/btn.vue'
+import { UnitCategory } from 'src/models/UnitCategory.js'
+import Breadcrumbs from 'src/components/Widgets/Breadcrumbs/Breadcrumbs.vue'
+import UsersOfTestSelector from 'src/components/FormBuilderCustumComponents/UsersOfTestSelector/UsersOfTestSelector.vue'
+import QuestionsCreateAndAppend from 'src/components/FormBuilderCustumComponents/QuestionsCreateAndAppend/QuestionsCreateAndAppend.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const UsersOfTestSelectorComp = shallowRef(UsersOfTestSelector)
+const QuestionsCreateAndAppendComp = shallowRef(QuestionsCreateAndAppend)
+
+export default {
+  name: 'AdminTestShow',
+  components: { Breadcrumbs, EntityEdit },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      mounted: false,
+      entityLoading: true,
+      test: new Test(),
+      classroom: new Classroom(),
+      api: APIGateway.test.APIAdresses.byId(this.$route.params.id),
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      classroomInputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات آزمون', col: 'col-12' },
+        { type: 'dateTime', name: 'start_time', responseKey: 'start_time', label: 'زمان شروع آزمون', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'dateTime', name: 'end_time', responseKey: 'end_time', label: 'زمان پایان آزمون', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'input', name: 'duration_deadline', responseKey: 'duration_deadline', label: 'مدت زمان پاسخ دهی (دقیقه)', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'dateTime', name: 'announcement_time', responseKey: 'announcement_time', label: 'زمان اعلام نتایج', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'dateTime', name: 'objection_start_time', responseKey: 'objection_start_time', label: 'زمان شروع اعتراض', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'dateTime', name: 'objection_end_time', responseKey: 'objection_end_time', label: 'زمان پایان اعتراض', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'select', name: 'level', responseKey: 'level', label: 'سطح آزمون', placeholder: ' ', options: (new Test()).levelEnums, col: 'col-md-3 col-12' },
+        { type: 'hidden', name: 'test_set', responseKey: 'test_set', value: this.$route.params.test_set_id },
+        { type: 'hidden', name: 'classroom', responseKey: 'classroom', value: this.$route.params.classroom_id },
+        { type: QuestionsCreateAndAppendComp, name: 'test_questions', responseKey: 'test_questions_info', testId: this.$route.params.id, questionType: 'QUESTION_BANK', ignoreValue: true, col: 'col-md-12 col-12' },
+        { type: UsersOfTestSelectorComp, name: 'examinees', responseKey: 'examinees', classroomId: this.$route.params.classroom_id, col: 'col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'تعیین جزییات', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' }
+      ],
+      eventInputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات آزمون', col: 'col-12' },
+        { type: QuestionsCreateAndAppendComp, name: 'test_questions', responseKey: 'test_questions_info', testId: this.$route.params.id, questionType: 'EVENT', ignoreValue: true, col: 'col-md-12 col-12' },
+        { type: 'hidden', name: 'event', responseKey: 'event', value: this.$route.params.classroom_id },
+        { type: 'hidden', name: 'test_set', responseKey: 'test_set', value: this.$route.params.test_set_id },
+        { type: 'hidden', name: 'classroom', responseKey: 'classroom', value: this.$route.params.classroom_id },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'تعیین جزییات', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' }
+      ],
+      inputs: []
+    }
+  },
+  computed: {
+    classroomTypeTitle () {
+      const unitCategory = new UnitCategory({ type: this.$route.query.classroom_type })
+      return unitCategory.type_info.label
+    },
+    indexPageRouteName () {
+      if (this.$route.query.classroom_type === 'TRAINING') {
+        return 'Admin.Classroom.Index'
+      }
+      if (this.$route.query.classroom_type === 'EVENT') {
+        return 'Admin.Event.Index'
+      }
+      return 'Admin.Classroom.Index'
+    },
+    showPageRouteName () {
+      if (this.$route.query.classroom_type === 'TRAINING') {
+        return 'Admin.Classroom.Show'
+      }
+      if (this.$route.query.classroom_type === 'EVENT') {
+        return 'Admin.Event.Show'
+      }
+      return 'Admin.Classroom.Show'
+    }
+  },
+  created () {
+    if (this.$route.query.classroom_type === 'EVENT') {
+      this.inputs = this.eventInputs
+    } else {
+      this.inputs = this.classroomInputs
+    }
+  },
+  mounted () {
+    this.getClassroom()
+    this.setActionBtn()
+    this.mounted = true
+  },
+  methods: {
+    setActionBtn () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.edit)
+    },
+    edit() {
+      this.entityLoading = true
+      this.$refs.entityEdit.editEntity()
+        .then(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.entityLoading = false
+        })
+    },
+    afterLoadInputData (responseData) {
+      this.test = new Test(responseData)
+      this.entityLoading = false
+    },
+    getClassroom () {
+      this.classroom.loading = true
+      APIGateway.classroom.get(this.$route.params.classroom_id)
+        .then((classroom) => {
+          this.classroom = new Classroom(classroom)
+          this.classroom.loading = false
+          this.updateBreadcrumbs()
+        })
+        .catch(() => {
+          this.classroom.loading = false
+        })
+    },
+    updateBreadcrumbs () {
+      this.$store.commit('AppLayout/updateBreadcrumbs', {
+        visible: true,
+        loading: false,
+        path: [
+          {
+            label: 'لیست ' + this.classroomTypeTitle,
+            to: { name: this.indexPageRouteName }
+          },
+          {
+            label: this.classroom.title,
+            to: { name: this.showPageRouteName, params: { id: this.$route.params.classroom_id } }
+          },
+          {
+            label: 'آزمون ها'
+          },
+          {
+            label: this.test.title
+          }
+        ]
+      })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminTestShow {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    position: relative;
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Test/AdminTestShow/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

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

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

@@ -0,0 +1,370 @@
+<template>
+  <div class="ShowTestGradeForGrader"
+       :style="localOptions.style">
+    <q-card v-if="!answerBook.loading && answerBook.id">
+      <q-card-section class="flex justify-between">
+        <div>{{ answerBook.test_info.title }}</div>
+      </q-card-section>
+      <q-separator />
+      <q-banner>
+        وضعیت آزمون:
+        {{ answerBook.status_info.label }}
+      </q-banner>
+      <q-btn v-if="!answerBook.grader"
+             color="primary"
+             class="full-width"
+             @click="setGraderToMe">
+        تصحیح توسط شما
+      </q-btn>
+      <q-separator />
+      <q-banner>
+        <div class="row">
+          <div class="col-md-4 col-12">
+            زمان شروع:
+            {{ toShamsi(answerBook.attending_start_time) }}
+          </div>
+          <div class="col-md-4 col-12">
+            {{ answerBook.owner_info.firstname }}
+            {{ answerBook.owner_info.lastname }}
+            (کدملی: {{ answerBook.owner_info.national_code }})
+          </div>
+          <div class="col-md-4 col-12">
+            نمره نهایی
+            <template v-if="currentUserIsGrader && answerBook.status === 'GRADING'">
+              {{ totalScore }}
+            </template>
+            <template v-else>
+              {{ answerBook.total_score }}
+            </template>
+          </div>
+        </div>
+      </q-banner>
+      <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-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>
+                  <div>
+                    پاسخ صحیح:
+                  </div>
+                  <div v-html="answerSheet.test_question_info.question_info.correct_answer" />
+                </q-card-section>
+              </q-card>
+              <q-card class="q-mt-sm">
+                <q-card-section>
+                  <div>
+                    متن پاسخ کاربر:
+                  </div>
+                  <div>
+                    {{ answerSheet.answer_text }}
+                  </div>
+                  <div v-if="answerSheet.answer_attachment">
+                    <q-separator class="q-my-md" />
+                    <q-btn outline
+                           color="primary"
+                           icon="attachment"
+                           label="فایل پیوست کاربر"
+                           :href="answerSheet.answer_attachment"
+                           target="_blank" />
+                  </div>
+                </q-card-section>
+              </q-card>
+            </div>
+            <div class="col-md-2 col-12">
+              <div>
+                <q-badge v-if="answerSheet.score !== null">
+                  {{ answerSheet.score }}
+                  از
+                  {{ answerSheet.test_question_info.mark }}
+                  نمره
+                </q-badge>
+                <q-badge v-else>
+                  {{ answerSheet.test_question_info.mark }}
+                  نمره
+                </q-badge>
+              </div>
+              <div v-if="currentUserIsGrader && answerBook.status === 'GRADING'"
+                   class="q-mt-md input-grate-question">
+                <q-input v-model="scores[answerSheetIndex].score"
+                         :loading="scores[answerSheetIndex].loading"
+                         dense>
+                  <template v-slot:append>
+                    <q-btn color="primary"
+                           label="ثبت نمره"
+                           outline
+                           :loading="scores[answerSheetIndex].loading"
+                           @click="submitScore(answerSheetIndex)" />
+                  </template>
+                </q-input>
+              </div>
+            </div>
+          </div>
+        </q-item>
+      </q-list>
+      <q-separator class="q-my-lg" />
+      <div class="q-pa-lg">
+        <entity-show v-if="mounted"
+                     ref="entityShow"
+                     v-model:value="overallAnswerInput"
+                     :loaded-data="answerBook"
+                     :show-close-button="false"
+                     :show-edit-button="false"
+                     :show-expand-button="false"
+                     :show-save-button="false"
+                     :show-index-button="false"
+                     :show-reload-button="false"
+                     :default-layout="false" />
+      </div>
+      <template v-if="$route.query.send_objection && $route.query.send_objection.toString() === '1'">
+        <q-separator class="q-my-xl" />
+        <q-card-section>
+          <entity-edit v-if="mounted"
+                       ref="objectionEntityEdit"
+                       v-model:value="objectionInputs"
+                       title="ثبت اعتراض"
+                       :api="objectionApi"
+                       :loaded-data="{}"
+                       :show-close-button="false"
+                       :show-edit-button="false"
+                       :show-expand-button="false"
+                       :show-save-button="false"
+                       :show-reload-button="false">
+            <template #after-form-builder>
+              <div class="flex justify-end q-mt-md">
+                <q-btn color="primary"
+                       label="ثبت اعتراض"
+                       @click="sendObjection" />
+              </div>
+            </template>
+          </entity-edit>
+        </q-card-section>
+      </template>
+      <template v-else-if="answerBook.objection_request">
+        <div>
+          متن اعتراض:
+        </div>
+        <div v-html="answerBook.objection_request" />
+      </template>
+      <template v-if="answerBook.objection_result">
+        <div>
+          نتیجه اعتراض:
+        </div>
+        <div v-html="answerBook.objection_result" />
+      </template>
+      <q-banner v-if="currentUserIsGrader && answerBook.status === 'GRADING'">
+        <q-input v-model="answerBook.grader_description"
+                 type="textarea"
+                 label="توضیحات مصحح"
+                 :loading="graderDescriptionLoading" />
+        <q-btn color="primary"
+               label="ثبت توضیحات مصحح"
+               outline
+               class="full-width"
+               :loading="graderDescriptionLoading"
+               @click="submitGraderDescription" />
+      </q-banner>
+      <q-card-actions v-if="answerBook.status === 'GRADING'">
+        <q-btn color="primary"
+               class="q-mr-lg"
+               @click="confirmScores">
+          تایید نهایی نمرات
+        </q-btn>
+        <q-btn color="red"
+               @click="confirmUngradable">
+          نیاز به رفع مشکل
+        </q-btn>
+      </q-card-actions>
+    </q-card>
+    <div v-else>
+      کمی صبر کنید...
+    </div>
+  </div>
+</template>
+
+<script>
+import Assist from 'src/assets/js/Assist.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { EntityShow, EntityEdit } from 'quasar-crud'
+import { AnswerBook } from 'src/models/AnswerBook.js'
+import { mixinWidget, mixinAuth } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'ShowTestGradeForGrader',
+  components: { EntityShow, EntityEdit },
+  mixins: [mixinWidget, mixinAuth],
+  data () {
+    return {
+      mounted: false,
+      graderDescriptionLoading: false,
+      currentUserIsGrader: false,
+      overallAnswerInput: [
+        // { type: 'input', name: 'overall_answer_text', responseKey: 'overall_answer_text', label: 'متن پاسخ جامع', placeholder: ' ', inputType: 'textarea', col: 'col-12' },
+        { type: 'file', name: 'overall_answer_attachment', responseKey: 'overall_answer_attachment', label: 'فایل پیوست جامع', placeholder: ' ', col: 'col-12' }
+      ],
+      questionInput: [
+        { type: 'input', name: 'answer_text', responseKey: 'answer_text', label: 'متن پاسخ', placeholder: ' ', inputType: 'textarea', col: 'col-12' },
+        { type: 'file', name: 'answer_attachment', responseKey: 'answer_attachment', label: 'فایل پیوست', placeholder: ' ', col: 'col-12' }
+      ],
+      questionInputs: [],
+      scores: [],
+      objectionInputs: [
+        { type: 'input', name: 'objection_request', responseKey: 'objection_request', label: 'متن اعتراض', placeholder: ' ', inputType: 'textarea', col: 'col-12' }
+      ],
+      objectionApi: APIGateway.answerBook.APIAdresses.submitObjectionRequest(this.$route.params.answer_book_id),
+      participateType: null,
+      participateTypeDialog: false,
+      answerBook: new AnswerBook(),
+      timerInterval: null
+    }
+  },
+  computed: {
+    totalScore () {
+      let totalScore = 0
+      this.scores.forEach(item => {
+        if (item.score) {
+          totalScore += parseFloat(item.score)
+        }
+      })
+
+      return totalScore
+    }
+  },
+  mounted () {
+    this.getAnswerBook()
+    this.mounted = true
+  },
+  methods: {
+    sendObjection () {
+      this.answerBook.loading = true
+      this.$refs.objectionEntityEdit.editEntity(false)
+        .then(() => {
+          this.getAnswerBook()
+        })
+        .catch(() => {
+          this.getAnswerBook()
+        })
+    },
+    toShamsi (miladi) {
+      return Assist.miladiToShamsi(miladi)
+    },
+    loadAnswerBook (answerBook) {
+      this.scores = []
+      this.answerBook = new AnswerBook(answerBook)
+      this.answerBook.answer_sheet_info.list.forEach((answerSheetItem) => {
+        this.questionInputs.push(this.questionInput)
+        this.scores.push({
+          answer_text: answerSheetItem.answer_text,
+          answer_attachment: answerSheetItem.answer_attachment,
+          score: answerSheetItem.score,
+          loading: false
+        })
+      })
+      this.currentUserIsGrader = this.answerBook.grader === this.user.id
+    },
+    setGraderToMe () {
+      this.answerBook.loading = true
+      APIGateway.answerBook.setGraderToMe(this.$route.params.answer_book_id)
+        .then((answerBook) => {
+          this.loadAnswerBook(answerBook)
+          this.answerBook.loading = false
+        })
+        .catch(() => {
+          this.getAnswerBook()
+          this.answerBook.loading = false
+        })
+    },
+    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.answerBook.answer_sheet_info.list[index].id, this.scores[index].score)
+        .then(() => {
+          this.scores[index].loading = false
+          this.getAnswerBook()
+        })
+        .catch(() => {
+          this.scores[index].loading = false
+        })
+    },
+    getAnswerBook () {
+      this.answerBook.loading = true
+      APIGateway.answerBook.get(this.$route.params.answer_book_id)
+        .then((answerBook) => {
+          this.loadAnswerBook(answerBook)
+          this.answerBook.loading = false
+        })
+        .catch(() => {
+          this.answerBook.loading = false
+        })
+    },
+    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
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.ShowTestGradeForGrader {
+  :deep(.q-banner) {
+    background: #FCF9F4 0 0 no-repeat padding-box;
+    padding: 20px;
+    .q-banner__content {
+      font-size: 16px;
+      font-weight: normal;
+    }
+  }
+  :deep(.q-list) {
+    .q-item {
+      .input-grate-question {
+        .q-input {
+          .q-field__inner .q-field__control {
+            height: 40px;
+            min-height: 40px;
+          }
+        }
+      }
+      .q-badge {
+        background: #ACBCAE 0 0 no-repeat padding-box;
+        border-radius: 18px;
+        height: 32px;
+        padding: 4px 16px;
+      }
+    }
+  }
+}
+</style>

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

+ 97 - 0
src/components/Widgets/Test/TestConfirmation/TestConfirmation.vue

@@ -0,0 +1,97 @@
+<template>
+  <div class="TestConfirmation"
+       :style="localOptions.style">
+    <q-card v-if="!answerBook.loading">
+      <q-card-section>
+        {{ answerBook.test_info.title }}
+      </q-card-section>
+      <q-separator />
+      <q-card-section v-html="answerBook.test_info.description" />
+      <q-card-actions class="flex justify-end">
+        <q-btn color="primary"
+               label="شرکت در آزمون"
+               @click="showParticipateTypeDialog" />
+      </q-card-actions>
+    </q-card>
+    <div v-else>
+      کمی صبر کنید...
+    </div>
+    <q-dialog v-model="participateTypeDialog">
+      <q-card>
+        <q-card-actions>
+          نحوه پاسخ به آزمون
+        </q-card-actions>
+        <q-separator />
+        <q-card-actions>
+          <div>
+            اندیشه جوی عزیز لطفا نحوه پاسخ به سوالات آزمون را مشخص کنید
+          </div>
+          <div>
+            <q-radio v-model="participateType"
+                     val="UserPanel.Test.AnswerBook.Participate.AllQuestions"
+                     label="مایلم پاسخ سوالات را به صورت یک فایل جامع بارگذاری کنم" />
+            <q-radio v-model="participateType"
+                     val="UserPanel.Test.AnswerBook.Participate.SingleQuestion"
+                     label="مایلم پاسخ سوالات را به صورت جداگانه ذیل همان سوال وارد کنم" />
+          </div>
+        </q-card-actions>
+        <q-card-actions>
+          <q-btn v-close-popup
+                 outline
+                 label="انصراف از آزمون"
+                 color="red" />
+          <q-btn :disable="!participateType"
+                 label="تایید و شروع آزمون"
+                 color="primary"
+                 @click="startTest" />
+        </q-card-actions>
+      </q-card>
+    </q-dialog>
+  </div>
+</template>
+
+<script>
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { AnswerBook } from 'src/models/AnswerBook.js'
+
+export default {
+  name: 'TestConfirmation',
+  mixins: [mixinWidget],
+  data () {
+    return {
+      participateType: null,
+      participateTypeDialog: false,
+      answerBook: new AnswerBook()
+    }
+  },
+  mounted () {
+    this.$store.commit('Test/updateAnswerBook', null)
+    this.getAnswerBook()
+  },
+  methods: {
+    showParticipateTypeDialog () {
+      this.participateTypeDialog = true
+    },
+    startTest () {
+      this.$router.push({ name: this.participateType, params: { question_number: 1 } })
+    },
+    getAnswerBook () {
+      this.answerBook.loading = true
+      APIGateway.answerBook.get(this.$route.params.answer_book_id)
+        .then((answerBook) => {
+          this.answerBook = new AnswerBook(answerBook)
+          this.answerBook.loading = false
+        })
+        .catch(() => {
+          this.answerBook.loading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.TestConfirmation {
+}
+</style>

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

+ 285 - 0
src/components/Widgets/Test/TestParticipation/TestParticipation.vue

@@ -0,0 +1,285 @@
+<template>
+  <div class="TestParticipation"
+       :style="localOptions.style">
+    <completion-page v-if="isCompletionPage"
+                     :answer-book="answerBook" />
+    <q-card v-else-if="!answerBook.loading && answerBook.id">
+      <q-card-section class="flex justify-between">
+        <div>{{ answerBook.test_info.title }}</div>
+        <div>
+          زمان باقیمانده تا پایان آزمون
+          {{ formattedRemainingTime }}
+        </div>
+      </q-card-section>
+      <q-separator />
+      <q-banner>
+        <div class="row">
+          <div class="col-md-4 col-12">
+            {{ toShamsi(answerBook.attending_start_time) }}
+          </div>
+          <div class="col-md-4 col-12">
+            {{ answerBook.test_info.test_questions_info.list.length }}
+            سوال
+          </div>
+        </div>
+      </q-banner>
+      <template v-if="mounted">
+        <all-questions v-if="$route.name === 'UserPanel.Test.AnswerBook.Participate.AllQuestions'"
+                       :answer-book="answerBook"
+                       @sentsuccess="onAllQuestionsSentSuccess"
+                       @sending="onAllQuestionsSending"
+                       @sentfailed="onAllQuestionsSentFailed" />
+        <single-question v-else-if="$route.name === 'UserPanel.Test.AnswerBook.Participate.SingleQuestion'"
+                         :answer-book="answerBook"
+                         :question-number="parseInt($route.params.question_number)"
+                         @sending="onSingleQuestionsSending"
+                         @sentSuccess="onSingleQuestionsSentSuccess"
+                         @sentFailed="onSingleQuestionsSentFailed" />
+        <div class="flex justify-end q-pa-lg">
+          <q-btn color="primary"
+                 outline
+                 @click="confirmAnswers">
+            اتمام آزمون
+          </q-btn>
+        </div>
+      </template>
+    </q-card>
+    <div v-else>
+      کمی صبر کنید...
+    </div>
+    <q-dialog v-model="participateTypeDialog">
+      <q-card>
+        <q-card-actions>
+          نحوه پاسخ به آزمون
+        </q-card-actions>
+        <q-separator />
+        <q-card-actions>
+          <div>
+            اندیشه جوی عزیز لطفا نحوه پاسخ به سوالات آزمون را مشخص کنید
+          </div>
+          <div>
+            <q-radio v-model="participateType"
+                     val="UserPanel.Test.AnswerBook.Participate.AllQuestions"
+                     label="مایلم پاسخ سوالات را به صورت یک فایل جامع بارگذاری کنم" />
+            <q-radio v-model="participateType"
+                     val="UserPanel.Test.AnswerBook.Participate.SingleQuestion"
+                     label="مایلم پاسخ سوالات را به صورت جداگانه ذیل همان سوال وارد کنم" />
+          </div>
+        </q-card-actions>
+        <q-card-actions>
+          <q-btn v-close-popup
+                 outline
+                 label="انصراف از آزمون"
+                 color="red" />
+          <q-btn :disable="!participateType"
+                 label="تایید و شروع آزمون"
+                 color="primary"
+                 @click="startTest" />
+        </q-card-actions>
+      </q-card>
+    </q-dialog>
+  </div>
+</template>
+
+<script>
+import Assist from 'src/assets/js/Assist.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { AnswerBook } from 'src/models/AnswerBook.js'
+import AllQuestions from './components/AllQuestions.vue'
+import SingleQuestion from './components/SingleQuestion.vue'
+import CompletionPage from './components/CompletionPage.vue'
+
+export default {
+  name: 'TestParticipation',
+  components: { AllQuestions, SingleQuestion, CompletionPage },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      mounted: false,
+      participateType: null,
+      participateTypeDialog: false,
+      storeAnswerBook: null,
+      answerBook: new AnswerBook(),
+      remainingTime: 0,
+      timerInterval: null
+    }
+  },
+  computed: {
+    isCompletionPage () {
+      return this.$route.name === 'UserPanel.Test.AnswerBook.Participate.SingleQuestion' && this.$route.params.question_number === 'complete'
+    },
+    formattedRemainingTime () {
+      const hours = Math.max(Math.floor(this.remainingTime / 3600), 0)
+      const minutes = Math.max(Math.floor((this.remainingTime % 3600) / 60), 0)
+      const seconds = Math.max(this.remainingTime % 60, 0)
+
+      // Add zero padding for hours, minutes, and seconds
+      const formattedHours = hours.toString().padStart(2, '0')
+      const formattedMinutes = minutes.toString().padStart(2, '0')
+      const formattedSeconds = seconds.toString().padStart(2, '0')
+
+      return formattedHours + ':' + formattedMinutes + ':' + formattedSeconds
+    }
+  },
+  mounted () {
+    this.getTestQuestions()
+    this.mounted = true
+
+    this.$bus.on('test-participate-single-question-sent-success', () => {
+      this.onSingleQuestionsSentSuccess()
+    })
+    this.$bus.on('test-participate-single-question-sent-failed', () => {
+      this.onSingleQuestionsSentFailed()
+    })
+    this.$bus.on('test-participate-all-question-sent-success', () => {
+      this.onAllQuestionsSentSuccess()
+    })
+    this.$bus.on('test-participate-all-question-sent-failed', () => {
+      this.onAllQuestionsSentFailed()
+    })
+  },
+  beforeUnmount() {
+    this.stopTimer()
+  },
+  methods: {
+    loadAnswerBookFromStore () {
+      this.storeAnswerBook = this.$store.getters['Test/answerBook']
+    },
+    onAllQuestionsSending () {
+      this.answerBook.loading = true
+    },
+    onAllQuestionsSentSuccess () {
+      this.answerBook.loading = false
+      this.getTestQuestions(true)
+    },
+    onAllQuestionsSentFailed () {
+      this.answerBook.loading = false
+    },
+    onSingleQuestionsSending () {
+      this.answerBook.loading = true
+    },
+    onSingleQuestionsSentSuccess () {
+      this.answerBook.loading = false
+      // this.backToClassList()
+    },
+    onSingleQuestionsSentFailed () {
+      this.answerBook.loading = false
+    },
+    toShamsi (miladi) {
+      return Assist.miladiToShamsi(miladi)
+    },
+
+    startTest () {
+      this.$router.push({ name: this.participateType, params: { question_number: 1 } })
+    },
+    confirmAnswers () {
+      this.$q.dialog({
+        title: 'اتمام آزمون',
+        message: 'آیا از اتمام آزمون و تایید پاسخ های خود اطمینان دارید؟',
+        cancel: true,
+        persistent: true
+      }).onOk(() => {
+        APIGateway.answerBook.confirmAnswers(this.answerBook.id)
+          .then(() => {
+            this.$q.notify({
+              message: 'تایید آزمون با موفقیت انجام شد.',
+              type: 'positive'
+            })
+            this.$router.push({
+              name: 'UserPanel.Test.AnswerBook.Participate.SingleQuestion',
+              params: {
+                test_id: this.$route.params.test_id,
+                answer_book_id: this.$route.params.answer_book_id,
+                question_number: 'complete'
+              }
+            })
+          })
+          .catch(() => {})
+      }).onCancel(() => {})
+    },
+    getRemainingTimeInSeconds (serverTimeStr, attendingStartTimeStr, durationInMinutes) {
+      // Convert the strings to Date objects
+      const serverTime = new Date(serverTimeStr)
+      const attendingStartTime = new Date(attendingStartTimeStr)
+
+      // Calculate the difference in milliseconds
+      const timeDiffInMilliseconds = serverTime - attendingStartTime
+
+      // Convert the difference to seconds
+      const timeDiffInSeconds = timeDiffInMilliseconds / 1000
+
+      // Calculate the remaining time in seconds considering the duration
+      return Math.floor((durationInMinutes * 60) - timeDiffInSeconds)
+    },
+    onTimeout () {
+      this.$q.notify({
+        type: 'negative',
+        message: 'زمان آزمون به اتمام رسیده است.'
+      })
+      this.stopTimer()
+      this.backToClassList()
+    },
+    startTimer () {
+      this.timerInterval = setInterval(() => {
+        if (this.remainingTime <= 0) {
+          this.onTimeout()
+        } else {
+          this.remainingTime--
+          this.answerBook.remainingTime = this.remainingTime
+          this.$store.commit('Test/updateAnswerBook', this.answerBook)
+        }
+      }, 1000)
+    },
+    stopTimer () {
+      if (!this.timerInterval) {
+        return
+      }
+
+      clearInterval(this.timerInterval)
+    },
+    backToClassList () {
+      this.$router.push({ name: 'UserPanel.Profile.AllClassrooms' })
+    },
+    initPageFromAnswerBook (answerBook) {
+      this.answerBook = new AnswerBook(answerBook)
+      this.answerBook.loading = false
+      if (this.answerBook.remainingTime === null) {
+        this.answerBook.remainingTime = this.getRemainingTimeInSeconds(this.answerBook.server_time, this.answerBook.attending_start_time, this.answerBook.test_info.duration_deadline)
+      }
+      this.remainingTime = this.answerBook.remainingTime
+      this.$store.commit('Test/updateAnswerBook', this.answerBook)
+      this.startTimer()
+    },
+    getTestQuestions (fresh = false) {
+      this.loadAnswerBookFromStore()
+      if (this.storeAnswerBook && this.storeAnswerBook.id && !fresh) {
+        this.initPageFromAnswerBook(this.storeAnswerBook, false)
+        return
+      }
+      this.answerBook.loading = true
+      APIGateway.answerBook.getTestQuestions(this.$route.params.answer_book_id)
+        .then((answerBook) => {
+          this.initPageFromAnswerBook(answerBook)
+        })
+        .catch(() => {
+          this.answerBook.loading = false
+          this.backToClassList()
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.TestParticipation {
+  :deep(.q-banner) {
+    background: #FCF9F4 0 0 no-repeat padding-box;
+    padding: 20px;
+    .q-banner__content {
+      font-size: 16px;
+      font-weight: normal;
+    }
+  }
+}
+</style>

+ 150 - 0
src/components/Widgets/Test/TestParticipation/components/AllQuestions.vue

@@ -0,0 +1,150 @@
+<template>
+  <div class="AllQuestions">
+    <q-list separator>
+      <q-item v-for="(answerSheet, answerSheetIndex) in answerBook.answer_sheet_info.list"
+              :key="answerSheetIndex"
+              clickable>
+        <div class="full-width">
+          <div class="flex justify-end">
+            <q-badge>
+              {{ answerSheet.test_question_info.question_info.mark }}
+              نمره
+            </q-badge>
+          </div>
+          <div v-html="'<span>' + (answerSheetIndex + 1) + ' - </span>' + answerSheet.test_question_info.question_info.text" />
+        </div>
+      </q-item>
+    </q-list>
+    <q-separator class="q-my-lg" />
+    <q-card-actions class="flex justify-end AllQuestionsForm">
+      <entity-create v-if="mounted"
+                     ref="entityCreate"
+                     v-model:value="inputs"
+                     :api="api"
+                     :default-layout="false"
+                     :show-close-button="false"
+                     :show-edit-button="false"
+                     :show-expand-button="false"
+                     :show-save-button="false"
+                     :show-reload-button="false" />
+    </q-card-actions>
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityCreate } from 'quasar-crud'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { AnswerBook } from 'src/models/AnswerBook.js'
+import BtnControl from 'src/components/Control/btn.vue'
+import { FormBuilderAssist } from 'quasar-form-builder'
+
+const BtnControlComp = shallowRef(BtnControl)
+
+export default {
+  name: 'AllQuestions',
+  components: { EntityCreate },
+  props: {
+    answerBook: {
+      type: AnswerBook,
+      default: new AnswerBook()
+    }
+  },
+  emits: ['sending', 'sentsuccess', 'sentfailed'],
+  data () {
+    return {
+      mounted: false,
+      api: APIGateway.answerBook.APIAdresses.submitOverallAnswer(this.$route.params.answer_book_id),
+      inputs: [
+        // { type: 'inputEditor', name: 'answer_text', label: 'متن پاسخ', placeholder: ' ', col: 'col-12' },
+        // { type: 'input', name: 'answer_text', label: 'متن پاسخ', placeholder: ' ', inputType: 'textarea', col: 'col-12' },
+        { type: 'file', name: 'answer_attachment', label: 'فایل جامع پاسخ سوالات', placeholder: ' ', sendNull: true, col: 'col-9' },
+        { type: BtnControlComp, name: 'btnRemoveAnswerAttachment', label: 'حذف فایل پیوست', placeholder: ' ', atClick: () => {}, col: 'col-3 flex justify-center' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ثبت پاسخ جامع', placeholder: ' ', atClick: () => {}, col: 'col-12' }
+      ]
+    }
+  },
+  mounted () {
+    this.setInputs()
+    this.mounted = true
+  },
+  methods: {
+    setInputs () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.sendAnswers)
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btnRemoveAnswerAttachment', 'atClick', () => { this.removeAnswerAttachment() })
+
+      FormBuilderAssist.setAttributeByName(this.inputs, 'answer_text', 'value', this.answerBook.overall_answer_text)
+      FormBuilderAssist.setAttributeByName(this.inputs, 'answer_attachment', 'value', this.answerBook.overall_answer_attachment)
+    },
+    removeAnswerAttachment () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'answer_attachment', 'value', null)
+    },
+    isFileSizeGtThan (targetSize) {
+      const file = FormBuilderAssist.getInputsByName(this.inputs, 'answer_attachment').value
+      const fileSizeinMb = file?.size ? file.size / 1000000 : 0
+      return fileSizeinMb > targetSize
+    },
+    isPdfFile () {
+      const file = FormBuilderAssist.getInputsByName(this.inputs, 'answer_attachment').value
+      return file?.type ? file.type === 'application/pdf' : true
+    },
+    sendAnswers () {
+      if (this.isFileSizeGtThan(10)) {
+        this.$q.notify({
+          type: 'negative',
+          message: 'حجم فایل نباید بیشتر از 10 مگابایت باشد.'
+        })
+        return
+      }
+      if (!this.isPdfFile()) {
+        this.$q.notify({
+          type: 'negative',
+          message: 'تنها امکان ارسال فایل pdf وجود دارد.'
+        })
+        return
+      }
+
+      this.$emit('sending')
+      this.$refs.entityCreate.createEntity(false)
+        .then((response) => {
+          const answerBook = new AnswerBook(response.data)
+          this.$store.commit('Test/updateAnswerBook', answerBook)
+          FormBuilderAssist.setAttributeByName(this.inputs, 'answer_text', 'value', answerBook.overall_answer_text)
+          FormBuilderAssist.setAttributeByName(this.inputs, 'answer_attachment', 'value', answerBook.overall_answer_attachment)
+          this.$emit('sentsuccess')
+          this.$bus.emit('test-participate-all-question-sent-success')
+        })
+        .catch((error) => {
+          this.$emit('sentfailed')
+          this.$bus.emit('test-participate-all-question-sent-failed')
+          if (error.response && error.response.status === 422) {
+            this.$router.push({ name: 'UserPanel.Profile.AllClassrooms' })
+          }
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AllQuestions {
+  :deep(.q-list) {
+    .q-item {
+      .q-item__section {
+        display: inline-block;
+      }
+      .q-badge {
+        background: #ACBCAE 0 0 no-repeat padding-box;
+        border-radius: 18px;
+        height: 32px;
+        padding: 4px 16px;
+      }
+    }
+  }
+  .AllQuestionsForm {
+    & > div {
+      width: 100%;
+    }
+  }
+}
+</style>

+ 85 - 0
src/components/Widgets/Test/TestParticipation/components/CompletionPage.vue

@@ -0,0 +1,85 @@
+<template>
+  <div class="CompletionPage">
+    <q-card v-if="!answerBook.loading && answerBook.id">
+      <q-card-section class="flex justify-between">
+        <div>{{ answerBook.test_info.title }}</div>
+      </q-card-section>
+      <q-separator />
+      <q-card-section class="flex column items-center justify-center">
+        <div class="q-mb-lg">
+          <q-icon name="check_circle"
+                  size="xl"
+                  color="green" />
+        </div>
+        <div class="q-mb-md">
+          {{ user.firstname }}
+          {{ user.lastname }}
+          عزیز آزمون شما با موفقیت انجام شد
+        </div>
+        <div v-if="answerBook.test_info.announcement_time"
+             class="q-mb-md">
+          زمان اعلام نتایج آزمون :
+          {{ toShamsi(answerBook.test_info.announcement_time) }}
+        </div>
+        <div v-if="answerBook.test_info.objection_start_time && answerBook.test_info.objection_end_time"
+             class="q-mb-md">
+          بازه اعتراض به نتایج آزمون :
+          {{ toShamsi(answerBook.test_info.objection_start_time) }}
+          لغایت
+          {{ toShamsi(answerBook.test_info.objection_end_time) }}
+        </div>
+        <div class="q-mb-md">
+          <q-btn color="primary"
+                 label="بازگشت"
+                 :to="{name: 'UserPanel.Profile.AllClassrooms'}" />
+        </div>
+      </q-card-section>
+    </q-card>
+  </div>
+</template>
+
+<script>
+import Assist from 'src/assets/js/Assist.js'
+import { mixinAuth } from 'src/mixin/Mixins.js'
+import { AnswerBook } from 'src/models/AnswerBook.js'
+
+export default {
+  name: 'SingleQuestion',
+  mixins: [mixinAuth],
+  props: {
+    answerBook: {
+      type: AnswerBook,
+      default: new AnswerBook()
+    },
+    questionNumber: {
+      type: Number,
+      default: 1
+    }
+  },
+  emits: ['sending', 'sentSuccess', 'sentFailed'],
+  data () {
+    return {
+      mounted: false
+    }
+  },
+  mounted () {
+    this.mounted = true
+  },
+  methods: {
+    toShamsi (miladi) {
+      return Assist.miladiToShamsi(miladi)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.SingleQuestion {
+  .q-badge {
+    background: #ACBCAE 0 0 no-repeat padding-box;
+    border-radius: 18px;
+    height: 32px;
+    padding: 4px 16px;
+  }
+}
+</style>

+ 213 - 0
src/components/Widgets/Test/TestParticipation/components/SingleQuestion.vue

@@ -0,0 +1,213 @@
+<template>
+  <div class="SingleQuestion">
+    <q-card-section>
+      <div class="question-text">
+        <span>سوال</span>
+        <span>{{ questionNumber }}</span>
+        <span>-</span>
+        <span>
+          <q-badge v-if="question"
+                   class="q-mx-sm">
+            {{ question.mark }}
+            نمره
+          </q-badge>
+        </span>
+
+        <span v-if="question.text"
+              v-html="question.text" />
+      </div>
+      <q-separator class="q-my-lg" />
+      <q-card-actions class="flex justify-end SingleQuestionForm">
+        <entity-create v-if="mounted"
+                       ref="entityCreate"
+                       v-model:value="inputs"
+                       :api="api"
+                       :default-layout="false"
+                       :show-close-button="false"
+                       :show-edit-button="false"
+                       :show-expand-button="false"
+                       :show-save-button="false"
+                       :show-reload-button="false" />
+      </q-card-actions>
+    </q-card-section>
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityCreate } from 'quasar-crud'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { AnswerBook } from 'src/models/AnswerBook.js'
+import BtnControl from 'src/components/Control/btn.vue'
+import { FormBuilderAssist } from 'quasar-form-builder'
+
+const BtnControlComp = shallowRef(BtnControl)
+
+export default {
+  name: 'SingleQuestion',
+  components: { EntityCreate },
+  props: {
+    answerBook: {
+      type: AnswerBook,
+      default: new AnswerBook()
+    },
+    questionNumber: {
+      type: Number,
+      default: 1
+    }
+  },
+  emits: ['sending', 'sentsuccess', 'sentfailed'],
+  data () {
+    return {
+      mounted: false,
+      api: APIGateway.answerSheet.APIAdresses.base,
+      btnPrev: { type: BtnControlComp, name: 'btnPrev', label: 'سوال قبلی', placeholder: ' ', outline: true, atClick: () => {}, col: 'col-md-6 col-12' },
+      btnNext: { type: BtnControlComp, name: 'btnNext', label: 'سوال بعدی', placeholder: ' ', atClick: () => {}, col: 'col-md-6 col-12 flex justify-end' },
+      // btnFinal: { type: BtnControlComp, name: 'btnFinal', label: 'اتمام آزمون', placeholder: ' ', atClick: () => {}, col: 'col-md-6 col-12 flex justify-end' },
+      inputs: [
+        { type: 'input', name: 'answer_text', label: 'متن پاسخ', placeholder: ' ', inputType: 'textarea', col: 'col-12' },
+        { type: 'file', name: 'answer_attachment', label: 'فایل پیوست', placeholder: ' ', sendNull: true, col: 'col-9' },
+        { type: BtnControlComp, name: 'btnRemoveAnswerAttachment', label: 'حذف فایل پیوست', placeholder: ' ', atClick: () => {}, col: 'col-3 flex justify-center' },
+        { type: BtnControlComp, name: 'btnSendAnswer', label: 'ثبت پاسخ این سوال', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-center' },
+        { type: 'hidden', name: 'answer_book', value: this.answerBook.id },
+        { type: 'hidden', name: 'test_question', value: null }
+      ]
+    }
+  },
+  computed: {
+    hasNextQuestion () {
+      return this.answerBook.test_info.test_questions_info.list.length > this.questionNumber
+    },
+    hasPrevQuestion () {
+      return this.questionNumber > 1
+    },
+    isLastQuestion () {
+      return this.answerBook.test_info.test_questions_info.list.length === this.questionNumber
+    },
+    testSetQuestion () {
+      return this.answerBook.answer_sheet_info.list[this.questionNumber - 1].test_question_info
+    },
+    question () {
+      return this.testSetQuestion.question_info
+    }
+  },
+  mounted () {
+    this.setInputs()
+    this.mounted = true
+    if (this.answerBook.test_info.test_questions_info.list.length < this.questionNumber || this.questionNumber < 1) {
+      this.$router.push({ name: 'UserPanel.Test.AnswerBook.Participate.AllQuestions', params: this.$route.params })
+    }
+  },
+  methods: {
+    setInputs () {
+      if (this.hasPrevQuestion) {
+        this.inputs.push(this.btnPrev)
+      }
+      if (this.hasNextQuestion) {
+        this.inputs.push(this.btnNext)
+      }
+      // if (this.isLastQuestion) {
+      //   this.inputs.push(this.btnFinal)
+      // }
+      if (this.hasPrevQuestion && !this.hasNextQuestion) {
+        FormBuilderAssist.setAttributeByName(this.inputs, 'btnPrev', 'col', 'col-md-6 col-12 flex justify-start')
+      }
+      if (!this.hasPrevQuestion && this.hasNextQuestion) {
+        FormBuilderAssist.setAttributeByName(this.inputs, 'btnNext', 'col', 'col-12 flex justify-end')
+      }
+
+      const questionIndex = this.questionNumber - 1
+      FormBuilderAssist.setAttributeByName(this.inputs, 'answer_text', 'value', this.answerBook.answer_sheet_info.list[questionIndex].answer_text)
+      FormBuilderAssist.setAttributeByName(this.inputs, 'answer_attachment', 'value', this.answerBook.answer_sheet_info.list[questionIndex].answer_attachment)
+
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btnSendAnswer', 'atClick', () => { this.sendAnswer() })
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btnRemoveAnswerAttachment', 'atClick', () => { this.removeAnswerAttachment() })
+
+      // FormBuilderAssist.setAttributeByName(this.inputs, 'btnFinal', 'atClick', () => { this.sendAnswer('final') })
+
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btnNext', 'atClick', () => { this.goToNextQuestion() })
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btnPrev', 'atClick', () => { this.goToPrevQuestion() })
+      FormBuilderAssist.setAttributeByName(this.inputs, 'test_question', 'value', this.testSetQuestion?.id)
+    },
+    removeAnswerAttachment () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'answer_attachment', 'value', null)
+    },
+    goToNextQuestion () {
+      this.goToQuestion(this.questionNumber + 1)
+    },
+    goToPrevQuestion () {
+      this.goToQuestion(this.questionNumber - 1)
+    },
+    isFileSizeGtThan (targetSize) {
+      const file = FormBuilderAssist.getInputsByName(this.inputs, 'answer_attachment').value
+      const fileSizeinMb = file?.size ? file.size / 1000000 : 0
+      return fileSizeinMb > targetSize
+    },
+    isImageFile () {
+      const file = FormBuilderAssist.getInputsByName(this.inputs, 'answer_attachment').value
+      const fileType = file?.type ? file.type : 'image/'
+      return fileType.startsWith('image/')
+    },
+    sendAnswer () {
+      if (this.isFileSizeGtThan(3)) {
+        this.$q.notify({
+          type: 'negative',
+          message: 'حجم فایل نباید بیشتر از 10 مگابایت باشد.'
+        })
+        return
+      }
+      if (!this.isImageFile()) {
+        this.$q.notify({
+          type: 'negative',
+          message: 'تنها امکان ارسال فایل تصویری وجود دارد.'
+        })
+        return
+      }
+      this.$emit('sending')
+      this.$refs.entityCreate.createEntity(false)
+        .then((response) => {
+          const answerBook = this.answerBook
+          const questionIndex = this.questionNumber - 1
+          answerBook.answer_sheet_info.list[questionIndex].answer_text = response.data.answer_text
+          answerBook.answer_sheet_info.list[questionIndex].answer_attachment = response.data.answer_attachment
+          this.$store.commit('Test/updateAnswerBook', answerBook)
+          this.$emit('sentsuccess')
+          this.$bus.emit('test-participate-single-question-sent-success')
+        })
+        .catch((error) => {
+          this.$emit('sentfailed')
+          this.$bus.emit('test-participate-single-question-sent-failed')
+          if (error.response && error.response.status === 422) {
+            this.$router.push({ name: 'UserPanel.Profile.AllClassrooms' })
+          }
+        })
+    },
+    goToQuestion (questionNumber) {
+      this.$router.push({
+        name: 'UserPanel.Test.AnswerBook.Participate.SingleQuestion',
+        params: {
+          test_id: this.$route.params.test_id,
+          answer_book_id: this.$route.params.answer_book_id,
+          question_number: questionNumber
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.SingleQuestion {
+  .q-badge {
+    background: #ACBCAE 0 0 no-repeat padding-box;
+    border-radius: 18px;
+    height: 32px;
+    padding: 4px 16px;
+  }
+  .SingleQuestionForm {
+    & > div {
+      width: 100%;
+    }
+  }
+}
+</style>

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

+ 303 - 0
src/components/Widgets/Test/TestShow/TestShow.vue

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

+ 109 - 0
src/components/Widgets/TestSet/AdminEventTestSetCreate/AdminEventTestSetCreate.vue

@@ -0,0 +1,109 @@
+<template>
+  <div class="AdminEventTestSetCreate"
+       :style="localOptions.style">
+    <div class="flex justify-end">
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-create v-if="mounted"
+                   ref="entityCreate"
+                   v-model:value="inputs"
+                   title="مشخصات پرسشنامه"
+                   :api="api"
+                   :entity-id-key="entityIdKey"
+                   :entity-param-key="entityParamKey"
+                   :show-route-name="showRouteName"
+                   :show-close-button="false"
+                   :show-edit-button="false"
+                   :show-expand-button="false"
+                   :show-save-button="false"
+                   :show-reload-button="false" />
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityCreate } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import BtnControl from 'src/components/Control/btn.vue'
+// import QuestionsSelector from 'src/components/FormBuilderCustumComponents/QuestionsSelector/QuestionsSelector.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+// const QuestionsSelectorComp = shallowRef(QuestionsSelector)
+
+export default {
+  name: 'AdminEventTestSetCreate',
+  components: { EntityCreate },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      mounted: false,
+      entityLoading: false,
+      api: APIGateway.testSet.APIAdresses.base,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Unit.TestSet.Show',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان پرسشنامه', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات پرسشنامه', col: 'col-md-12 col-12' },
+        { type: 'hidden', name: 'type', responseKey: 'type', value: 'QUESTIONNAIRE' },
+        // { type: BtnControlComp, name: 'btnCreateQuestion', responseKey: 'btnCreateQuestion', label: 'تعریف سوال جدید', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' },
+        // { type: QuestionsSelectorComp, name: 'test_set_questions', responseKey: 'test_set_questions', unitId: false, questionType: 'EVENT', col: 'col-md-12 col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'تایید نهایی', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' }
+      ]
+    }
+  },
+  mounted() {
+    this.setActionBtn()
+    this.mounted = true
+  },
+  methods: {
+    setActionBtn () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.create)
+      // FormBuilderAssist.setAttributeByName(this.inputs, 'btnCreateQuestion', 'atClick', this.goToCreateQuestionPage)
+    },
+    goToCreateQuestionPage () {
+      this.$router.push({ name: 'Admin.Event.TestSet.Questions.Create', params: { test_set_id: this.$route.params.test_set_id } })
+    },
+    create() {
+      this.entityLoading = true
+      this.$refs.entityCreate.createEntity(false)
+        .then((response) => {
+          this.$router.push({ name: 'Admin.Event.TestSet.Show', params: { test_set_id: response.data.id } })
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminEventTestSetCreate {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    position: relative;
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

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

+ 168 - 0
src/components/Widgets/TestSet/AdminEventTestSetIndex/AdminEventTestSetIndex.vue

@@ -0,0 +1,168 @@
+<template>
+  <div>
+    <div class="flex justify-end">
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-index v-if="mounted"
+                  ref="testSetList"
+                  v-model:value="testSetListInputs"
+                  title="لیست پرسشنامه ها"
+                  :api="testSetListApi"
+                  :table="testSetListTable"
+                  :table-keys="testSetListTableKeys"
+                  :show-search-button="false"
+                  :show-reload-button="false"
+                  :show-expand-button="false">
+      <template #toolbar>
+        <q-btn color="primary"
+               outline
+               :to="{name: 'Admin.Event.TestSet.Create'}">
+          تعریف پرسشنامه جدید
+        </q-btn>
+      </template>
+      <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.Event.TestSet.Show', params: {test_set_id:inputData.props.row.id}}"
+                   :loading="loading"
+                   class="q-mr-md" />
+            <!--            <q-btn size="md"-->
+            <!--                   color="primary"-->
+            <!--                   label="انتخاب"-->
+            <!--                   :loading="loading"-->
+            <!--                   class="q-mr-md"-->
+            <!--                   @click="selectTestSet(inputData.props.row.id)" />-->
+          </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'
+import { FormBuilderAssist } from 'quasar-form-builder'
+
+export default {
+  name: 'AdminEventTestSetIndex',
+  components: {
+    EntityIndex
+  },
+  mixins: [mixinAuthData],
+  data () {
+    return {
+      mounted: false,
+      loading: false,
+      classroom: new Classroom(),
+
+      dropClassroomByAdminLoading: false,
+      exportReportLoading: false,
+      testSetListInputs: [
+        { type: 'hidden', name: 'type', value: 'QUESTIONNAIRE' }
+      ],
+      testSetListApi: APIGateway.testSet.APIAdresses.base,
+      testSetListTable: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'id',
+            required: true,
+            label: 'شناسه',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'عنوان پرسشنامه',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'test_set_questions.length',
+            required: true,
+            label: 'تعداد سوالات',
+            align: 'left',
+            field: row => row.test_set_questions.length
+          },
+          {
+            name: 'beginning_time',
+            required: true,
+            label: 'تاریخ ایجاد',
+            align: 'left',
+            field: row => row.creation_time ? ShamsiDate.getDateTime(row.creation_time) : '-'
+          },
+          {
+            name: 'creator_info',
+            required: true,
+            label: 'ایجاد کننده',
+            align: 'left',
+            field: row => row.creator_info.firstname + ' ' + row.creator_info.lastname
+          },
+          {
+            name: 'actions',
+            required: true,
+            label: 'انتخاب',
+            align: 'left',
+            field: ''
+          }
+        ]
+      },
+      testSetListTableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      }
+    }
+  },
+  mounted () {
+    this.setMembersListActionBtn()
+    FormBuilderAssist.setAttributeByName(this.testSetListInputs, 'leader', 'value', this.user.id)
+    this.mounted = true
+  },
+  methods: {
+    setMembersListActionBtn () {
+      FormBuilderAssist.setAttributeByName(this.testSetListInputs, 'btn', 'atClick', this.searchMembersList)
+    },
+    selectTestSet () {
+      // this.loading = true
+      // APIGateway.test.createTestForEvent (testTitle, eventId, tstSetId)
+      // .then(()=>{
+      //   this.loading = false
+      // })
+    },
+    searchMembersList () {
+      this.$refs.testSetList.search()
+    }
+  }
+}
+</script>

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

+ 113 - 0
src/components/Widgets/TestSet/AdminEventTestSetShow/AdminEventTestSetShow.vue

@@ -0,0 +1,113 @@
+<template>
+  <div class="AdminEventTestSetShow"
+       :style="localOptions.style">
+    <div class="flex justify-end">
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-edit v-if="mounted"
+                 ref="entityEdit"
+                 v-model:value="inputs"
+                 title="مشخصات پرسشنامه"
+                 :api="api"
+                 :entity-id-key="entityIdKey"
+                 :entity-param-key="entityParamKey"
+                 :show-close-button="false"
+                 :show-edit-button="false"
+                 :show-expand-button="false"
+                 :show-save-button="false"
+                 :show-reload-button="false"
+                 :redirect-after-edit="false" />
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityEdit } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import BtnControl from 'src/components/Control/btn.vue'
+// import QuestionsSelector from 'src/components/FormBuilderCustumComponents/QuestionsSelector/QuestionsSelector.vue'
+import QuestionsCreateAndAppend from 'src/components/FormBuilderCustumComponents/QuestionsCreateAndAppend/QuestionsCreateAndAppend.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+// const QuestionsSelectorComp = shallowRef(QuestionsSelector)
+const QuestionsCreateAndAppendComp = shallowRef(QuestionsCreateAndAppend)
+
+export default {
+  name: 'AdminEventTestSetShow',
+  components: { EntityEdit },
+  mixins: [mixinWidget],
+  data () {
+    const testSetId = this.$route.params.test_set_id
+    return {
+      mounted: false,
+      entityLoading: false,
+      api: APIGateway.testSet.APIAdresses.byId(testSetId),
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان پرسشنامه', placeholder: ' ', col: 'col-md-12 col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات پرسشنامه', col: 'col-md-12 col-12' },
+        { type: 'hidden', name: 'id', responseKey: 'id' },
+        { type: 'hidden', name: 'type', responseKey: 'type', value: 'QUESTIONNAIRE' },
+        // { type: BtnControlComp, name: 'btnCreateQuestion', responseKey: 'btnCreateQuestion', label: 'تعریف سوال جدید', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' },
+        // { type: QuestionsSelectorComp, name: 'test_set_questions', responseKey: 'test_set_questions', unitId: false, questionType: 'EVENT', col: 'col-md-12 col-12' },
+        { type: QuestionsCreateAndAppendComp, name: 'test_set_questions', responseKey: 'test_set_questions', testSetId, questionType: 'EVENT', ignoreValue: true, col: 'col-md-12 col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'تایید نهایی', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' }
+      ]
+    }
+  },
+  mounted() {
+    this.setActionBtn()
+    this.mounted = true
+  },
+  methods: {
+    setActionBtn () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.edit)
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btnCreateQuestion', 'atClick', this.goToCreateQuestionPage)
+    },
+    goToCreateQuestionPage () {
+      this.$router.push({ name: 'Admin.Event.TestSet.Questions.Create', params: { test_set_id: this.$route.params.test_set_id } })
+    },
+    edit() {
+      this.entityLoading = true
+      this.$refs.entityEdit.editEntity()
+        .then(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminEventTestSetShow {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    position: relative;
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

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

+ 105 - 0
src/components/Widgets/TestSet/AdminTestSetCreate/AdminTestSetCreate.vue

@@ -0,0 +1,105 @@
+<template>
+  <div class="AdminTestCreate"
+       :style="localOptions.style">
+    <div class="flex justify-end">
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-create v-if="mounted"
+                   ref="entityCreate"
+                   v-model:value="inputs"
+                   title="مشخصات آزمون"
+                   :api="api"
+                   :entity-id-key="entityIdKey"
+                   :entity-param-key="entityParamKey"
+                   :show-route-name="showRouteName"
+                   :show-close-button="false"
+                   :show-edit-button="false"
+                   :show-expand-button="false"
+                   :show-save-button="false"
+                   :show-reload-button="false" />
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityCreate } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import BtnControl from 'src/components/Control/btn.vue'
+import QuestionsSelector from 'src/components/FormBuilderCustumComponents/QuestionsSelector/QuestionsSelector.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const QuestionsSelectorComp = shallowRef(QuestionsSelector)
+
+export default {
+  name: 'AdminTestCreate',
+  components: { EntityCreate },
+  mixins: [mixinWidget],
+  data () {
+    const unitId = this.$route.params.unit_id
+    return {
+      mounted: false,
+      entityLoading: false,
+      api: APIGateway.testSet.APIAdresses.base,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Unit.TestSet.Show',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان آزمون', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات آزمون', col: 'col-md-12 col-12' },
+        { type: 'hidden', name: 'unit', responseKey: 'unit', value: unitId },
+        { type: QuestionsSelectorComp, name: 'test_set_questions', responseKey: 'test_set_questions', unitId, col: 'col-md-12 col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'تایید نهایی', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' }
+      ]
+    }
+  },
+  mounted() {
+    this.setActionBtn()
+    this.mounted = true
+  },
+  methods: {
+    setActionBtn () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.create)
+    },
+    create() {
+      this.entityLoading = true
+      this.$refs.entityCreate.createEntity()
+        .then((response) => {
+          this.$router.push({ name: 'Admin.Unit.TestSet.Show', params: { unit_id: this.$route.params.unit_id, id: response.data.id } })
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminTestCreate {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    position: relative;
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/TestSet/AdminTestSetCreate/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

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

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

+ 21 - 0
src/components/Widgets/TestSet/AdminTestSetIndex/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 178 - 0
src/components/Widgets/TestSet/AdminTestSetShow/AdminTestSetShow.vue

@@ -0,0 +1,178 @@
+<template>
+  <div class="AdminTestShow"
+       :style="localOptions.style">
+    <q-skeleton v-if="!unit.id || !testSet.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 { TestSet } from 'src/models/TestSet.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'
+import QuestionsSelector from 'src/components/FormBuilderCustumComponents/QuestionsSelector/QuestionsSelector.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const QuestionsSelectorComp = shallowRef(QuestionsSelector)
+
+export default {
+  name: 'AdminTestShow',
+  components: { EntityEdit, Breadcrumbs },
+  mixins: [mixinWidget],
+  data () {
+    const unitId = this.$route.params.unit_id
+    const testSetId = this.$route.params.id
+    return {
+      mounted: false,
+      unit: new Unit(),
+      testSet: new TestSet(),
+      entityLoading: false,
+      api: APIGateway.testSet.APIAdresses.byId(testSetId),
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان آزمون', placeholder: ' ', col: 'col-md-12 col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات آزمون', col: 'col-md-12 col-12' },
+        { type: 'hidden', name: 'id', responseKey: 'id' },
+        { type: 'hidden', name: 'unit', responseKey: 'unit' },
+        { type: QuestionsSelectorComp, name: 'test_set_questions', responseKey: 'test_set_questions', unitId, col: 'col-md-12 col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'تایید نهایی', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' }
+      ]
+    }
+  },
+  mounted() {
+    this.setActionBtn()
+    this.mounted = true
+    Promise.all([
+      this.getUnit(),
+      this.getTestSet()
+    ])
+      .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()
+          })
+      })
+    },
+    getTestSet () {
+      return new Promise((resolve, reject) => {
+        this.testSet.loading = true
+        APIGateway.testSet.get(this.$route.params.id)
+          .then((testSet) => {
+            this.testSet = new TestSet(testSet)
+            this.testSet.loading = false
+            resolve()
+          })
+          .catch(() => {
+            this.testSet.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
+          }
+        ]
+      })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminTestShow {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    position: relative;
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/TestSet/AdminTestSetShow/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 0 - 1
src/components/other/VerifyMobileNumber.vue

@@ -5,7 +5,6 @@
     </div>
     <div class="contents">
       <q-input v-model="textInput"
-               :placeholder="placeholder"
                :disable="disable"
                :loading="loading"
                :class="{'verified': state === 'verified'}"

+ 125 - 0
src/models/AnswerBook.js

@@ -0,0 +1,125 @@
+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'
+
+class AnswerBook extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      { key: 'attachment' },
+      { key: 'attending_end_time' },
+      { key: 'attending_start_time' },
+      { key: 'creation_time' },
+      { key: 'creator' },
+      {
+        key: 'creator_info',
+        relatedModel: User
+      },
+      { key: 'owner' },
+      {
+        key: 'owner_info',
+        relatedModel: User
+      },
+      { key: 'test' },
+      {
+        key: 'test_info',
+        relatedModel: Test
+      },
+      { key: 'duration' },
+      { key: 'total_score' },
+      { key: 'grader' },
+      { key: 'grader_description' },
+      { key: 'grader_info' },
+      { key: 'is_active' },
+      { key: 'is_deleted' },
+      { key: 'last_modification_time' },
+      { key: 'objection_request' },
+      { key: 'objection_result' },
+      { key: 'objection_time' },
+      {
+        key: 'answer_sheet_info',
+        relatedModel: AnswerSheetList
+      },
+      { key: 'overall_answer_text' },
+      { key: 'overall_answer_attachment' },
+      { key: 'registration' },
+      { key: 'registration_info' },
+      { key: 'server_time' },
+      { key: 'attending_start_time' },
+      { key: 'status' },
+      { key: 'remainingTime' },
+      {
+        key: 'is_enabled_attending',
+        default: false
+      },
+      {
+        key: 'is_enabled_continuing',
+        default: false
+      },
+      {
+        key: 'is_enabled_objecting',
+        default: false
+      },
+      {
+        key: 'is_enabled_viewing',
+        default: false
+      }
+    ], {
+      status: {
+        infoKey: 'status_info',
+        enumListKey: 'statusEnums',
+        enums: [
+          {
+            label: 'جاری',
+            value: 'ONGOING'
+          },
+          {
+            label: 'حذف شده',
+            value: 'DELETED'
+          },
+          {
+            label: 'ایجاد شده',
+            value: 'CREATED'
+          },
+          {
+            label: 'ارسال نشده',
+            value: 'FETCHED'
+          },
+          {
+            label: 'ارسال شده',
+            value: 'SUBMITTED'
+          },
+          {
+            label: 'در حال تصحیح',
+            value: 'GRADING'
+          },
+          {
+            label: 'نیاز به رفع مشکل',
+            value: 'UNGRADABLE'
+          },
+          {
+            label: 'تصحیح شده',
+            value: 'GRADED'
+          },
+          {
+            label: 'اعتراض شده',
+            value: 'OBJECTED'
+          },
+          {
+            label: 'نهایی شده',
+            value: 'FINALIZED'
+          }
+        ]
+      }
+    })
+  }
+}
+
+class AnswerBookList extends Collection {
+  model () {
+    return AnswerBook
+  }
+}
+
+export { AnswerBook, AnswerBookList }

+ 27 - 0
src/models/AnswerSheet.js

@@ -0,0 +1,27 @@
+import { Collection, Model } from 'js-abstract-model'
+import { TestSetQuestion } from 'src/models/TestSetQuestion.js'
+
+class AnswerSheet extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      { key: 'answer_text' },
+      { key: 'answer_attachment' },
+      { key: 'answer_choice_index' },
+      { key: 'score' },
+      { key: 'test_question' },
+      {
+        key: 'test_question_info',
+        relatedModel: TestSetQuestion
+      }
+    ])
+  }
+}
+
+class AnswerSheetList extends Collection {
+  model () {
+    return AnswerSheet
+  }
+}
+
+export { AnswerSheet, AnswerSheetList }

+ 4 - 0
src/models/Classroom.js

@@ -28,6 +28,10 @@ class Classroom extends Model {
         key: 'professor_info',
         relatedModel: User
       },
+      {
+        key: 'tests',
+        default: []
+      },
       { key: 'unit' },
       {
         key: 'unit_info',

+ 4 - 0
src/models/Event.js

@@ -28,6 +28,10 @@ class Event extends Model {
         key: 'professor_info',
         relatedModel: User
       },
+      {
+        key: 'tests',
+        default: []
+      },
       { key: 'unit' },
       {
         key: 'unit_info',

+ 86 - 0
src/models/Question.js

@@ -0,0 +1,86 @@
+import { Collection, Model } from 'js-abstract-model'
+
+class Question extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      { key: 'text' },
+      { key: 'correct_answer' },
+      { key: 'mark' },
+      { key: 'level' },
+      { key: 'model_type' },
+      {
+        key: 'choices',
+        default: []
+      },
+      {
+        key: 'choices_info',
+        default: []
+      },
+      { key: 'correct_choice_index' },
+      {
+        key: 'type',
+        default: 'QUESTION_BANK'
+      }
+    ], {
+      level: {
+        infoKey: 'level_info',
+        enumListKey: 'levelEnums',
+        enums: [
+          {
+            label: 'سطح یک',
+            value: 'LEVEL1'
+          },
+          {
+            label: 'سطح دو',
+            value: 'LEVEL2'
+          },
+          {
+            label: 'سطح سه',
+            value: 'LEVEL3'
+          },
+          {
+            label: 'سطح چهار',
+            value: 'LEVEL4'
+          },
+          {
+            label: 'سطح پنج',
+            value: 'LEVEL5'
+          }
+        ]
+      },
+      type: {
+        infoKey: 'type_info',
+        enumListKey: 'typeEnums',
+        enums: [
+          {
+            label: 'بانک سوال',
+            value: 'QUESTION_BANK'
+          }
+        ]
+      },
+      model_type: {
+        infoKey: 'model_type_info',
+        enumListKey: 'model_typeEnums',
+        enums: [
+          {
+            label: 'تشریحی',
+            value: 'OPEN_ENDED'
+          },
+          {
+            label: 'چند گزینه ای',
+            value: 'MULTI_CHOICE'
+          }
+        ]
+      }
+    })
+  }
+}
+
+class QuestionList extends Collection {
+  model () {
+    return Question
+  }
+}
+
+export { Question, QuestionList }

+ 19 - 0
src/models/QuestionChoice.js

@@ -0,0 +1,19 @@
+import { Collection, Model } from 'js-abstract-model'
+
+class QuestionChoice extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      // { key: 'question' },
+      { key: 'text' }
+    ])
+  }
+}
+
+class QuestionChoiceList extends Collection {
+  model () {
+    return QuestionChoice
+  }
+}
+
+export { QuestionChoice, QuestionChoiceList }

+ 4 - 0
src/models/Registration.js

@@ -48,6 +48,10 @@ class Registration extends Model {
           {
             label: 'حذف سیستمی (شرایط غیر مجاز)',
             value: 'DROPPED_FOR_CONDITION'
+          },
+          {
+            label: 'حذف سیستمی (نهایی نشده)',
+            value: 'DROPPED_FOR_NOT_FINALIZING'
           }
         ]
       },

+ 109 - 0
src/models/Test.js

@@ -0,0 +1,109 @@
+import { TestSet } from 'src/models/TestSet.js'
+import { Collection, Model } from 'js-abstract-model'
+import { QuestionList } from 'src/models/Question.js'
+
+class Test extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      { key: 'title' },
+      { key: 'description' },
+      { key: 'level' },
+      { key: 'unit' },
+      { key: 'start_time' },
+      { key: 'end_time' },
+      { key: 'duration_deadline' },
+      { key: 'status' },
+      { key: 'test_set' },
+      {
+        key: 'test_set_info',
+        relatedModel: TestSet
+      },
+      {
+        key: 'test_questions',
+        default: []
+      },
+      {
+        key: 'test_questions_info',
+        relatedModel: QuestionList
+      }
+    ], {
+      level: {
+        infoKey: 'level_info',
+        enumListKey: 'levelEnums',
+        enums: [
+          {
+            label: 'سطح یک',
+            value: 'LEVEL1'
+          },
+          {
+            label: 'سطح دو',
+            value: 'LEVEL2'
+          },
+          {
+            label: 'سطح سه',
+            value: 'LEVEL3'
+          },
+          {
+            label: 'سطح چهار',
+            value: 'LEVEL4'
+          },
+          {
+            label: 'سطح پنج',
+            value: 'LEVEL5'
+          }
+        ]
+      },
+      status: {
+        infoKey: 'status_info',
+        enumListKey: 'statusEnums',
+        enums: [
+          {
+            label: 'ساخته شده',
+            value: 'CREATED'
+          },
+          {
+            label: 'قبل از برگزاری',
+            value: 'BEFORE_HOLDING'
+          },
+          {
+            label: 'برگزاری',
+            value: 'HOLDING'
+          },
+          {
+            label: 'قبل از اعلام نتایج',
+            value: 'BEFORE_ANNOUNCEMENT'
+          },
+          {
+            label: 'اعلام نتایج',
+            value: 'ANNOUNCEMENT'
+          },
+          {
+            label: 'دریافت اعتراضات',
+            value: 'OBJECTION'
+          },
+          {
+            label: 'رسیدگی به اعتراضات',
+            value: 'OBJECTION_ANNOUNCEMENT'
+          },
+          {
+            label: 'خاتمه',
+            value: 'CLOSED'
+          },
+          {
+            label: 'خاتمه یافته',
+            value: 'FINISHED'
+          }
+        ]
+      }
+    })
+  }
+}
+
+class TestList extends Collection {
+  model () {
+    return Test
+  }
+}
+
+export { Test, TestList }

+ 52 - 0
src/models/TestSet.js

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

+ 25 - 0
src/models/TestSetQuestion.js

@@ -0,0 +1,25 @@
+import { Collection, Model } from 'js-abstract-model'
+import { Question } from 'src/models/Question.js'
+
+class TestSetQuestion extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      { key: 'is_extra_mark' },
+      { key: 'mark' },
+      { key: 'question' },
+      {
+        key: 'question_info',
+        relatedModel: Question
+      }
+    ])
+  }
+}
+
+class TestSetQuestionList extends Collection {
+  model () {
+    return TestSetQuestion
+  }
+}
+
+export { TestSetQuestion, TestSetQuestionList }

+ 23 - 0
src/pages/Admin/Classroom/ClassroomAnswerBookShowGrade.vue

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

+ 23 - 0
src/pages/Admin/Classroom/ClassroomAnswerBookShowGradeForGrader.vue

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

+ 23 - 0
src/pages/Admin/Classroom/ClassroomAnswerBookSubmitGrade.vue

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

+ 23 - 0
src/pages/Admin/Classroom/ClassroomAnswerBooks.vue

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

+ 23 - 0
src/pages/Admin/Classroom/ClassroomAnswerBooksForGrader.vue

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

+ 23 - 0
src/pages/Admin/Classroom/MyAsGraderClassrooms.vue

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

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików