ali 2 лет назад
Родитель
Сommit
9ec9ad52c4

+ 2 - 0
src/api/APIGateway.js

@@ -19,6 +19,7 @@ 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'
@@ -61,6 +62,7 @@ export const APIGateway = {
   classroom: new ClassroomAPI(),
   answerBook: new AnswerBookApi(),
   instalment: new InstalmentAPI(),
+  answerSheet: new AnswerSheetApi(),
   transaction: new TransactionAPI(),
   pageSetting: new PageSettingAPI(),
   registration: new RegistrationAPI(),

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

@@ -4,11 +4,12 @@ import { AnswerBook, AnswerBookList } from 'src/models/AnswerBook.js'
 
 export default class AnswerBookAPI extends APIRepository {
   constructor() {
-    super('test_sets', appApi)
+    super('answer_books', appApi)
     this.APIAdresses = {
       base: '/lma/answer_books',
       byId: (id) => '/lma/answer_books/' + id,
-      getTestQuestions: (id) => '/lma/answer_books/' + id + '/get_test_questions'
+      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,

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

@@ -0,0 +1,90 @@
+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
+      }
+    })
+  }
+}

+ 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

+ 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>

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

@@ -0,0 +1,98 @@
+<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>
+        {{ answerBook.test_info.description }}
+      </q-card-section>
+      <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.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>

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

@@ -0,0 +1,237 @@
+<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_set_info.test_set_questions.length }}
+            سوال
+          </div>
+          <div class="col-md-4 col-12">
+            {{ answerBook.duration }}
+            دقیقه
+          </div>
+        </div>
+      </q-banner>
+      <template v-if="mounted">
+        <all-questions v-if="$route.name === 'UserPanel.Test.AnswerBook.Participate.AllQuestions'"
+                       :answer-book="answerBook"
+                       @sending="onAllQuestionsSending"
+                       @sentSuccess="onAllQuestionsSentSuccess"
+                       @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" />
+      </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,
+      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.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: {
+    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_number: 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
+    },
+    startTimer () {
+      this.timerInterval = setInterval(() => {
+        if (this.remainingTime <= 0) {
+          this.stopTimer()
+        } else {
+          this.remainingTime--
+        }
+      }, 1000)
+    },
+    stopTimer () {
+      if (!this.timerInterval) {
+        return
+      }
+
+      clearInterval(this.timerInterval)
+    },
+    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)
+          if (this.answerBook.test_info.test_set_questions_length === 0) {
+            this.$q.notify({
+              type: 'negative',
+              message: 'لیست سوالات آزمون خالی است.'
+            })
+            this.backToClassList()
+          }
+          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.server_time = '2023-12-15T23:57:46.909211'
+          this.answerBook.attending_start_time = '2023-12-15T23:27:46.909211'
+          this.remainingTime = this.getRemainingTimeInSeconds(this.answerBook.server_time, this.answerBook.attending_start_time, this.answerBook.duration)
+          this.startTimer()
+        })
+        .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>

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

@@ -0,0 +1,107 @@
+<template>
+  <div class="AllQuestions">
+    <q-list separator>
+      <q-item v-for="(question, questionIndex) in answerBook.test_info.test_set_info.test_set_questions"
+              :key="questionIndex"
+              clickable>
+        <q-item-section v-html="question.question_info.text" />
+        <q-item-section side>
+          <q-badge>
+            {{ question.mark }}
+            نمره
+          </q-badge>
+        </q-item-section>
+      </q-item>
+    </q-list>
+    <q-separator class="q-my-lg" />
+    <q-card-actions class="flex justify-end">
+      <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: 'file', name: 'answer_attachment', label: 'فایل جامع پاسخ سوالات', placeholder: ' ', col: 'col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'اتمام آزمون', placeholder: ' ', atClick: () => {}, col: 'col-12' }
+      ]
+    }
+  },
+  mounted () {
+    this.setBtnAction()
+    this.mounted = true
+  },
+  methods: {
+    setBtnAction () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.sendAnswers)
+    },
+    sendAnswers () {
+      const answerText = FormBuilderAssist.getInputsByName(this.inputs, 'answer_text').value
+      if (!answerText) {
+        this.$q.notify({
+          type: 'negative',
+          message: 'لطفا متن پاسخ را کامل کنید.'
+        })
+        return
+      }
+
+      this.$emit('sending')
+      this.$refs.entityCreate.createEntity(false)
+        .then(() => {
+          this.$emit('sentSuccess')
+        })
+        .catch(() => {
+          this.$emit('sentFailed')
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AllQuestions {
+  :deep(.q-list) {
+    .q-item {
+      .q-badge {
+        background: #ACBCAE 0 0 no-repeat padding-box;
+        border-radius: 18px;
+        height: 32px;
+        padding: 4px 16px;
+      }
+    }
+  }
+}
+</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>

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

@@ -0,0 +1,175 @@
+<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?.question_info"
+              v-html="question.question_info.text" />
+      </div>
+      <q-separator class="q-my-lg" />
+      <q-card-actions class="flex justify-end">
+        <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: 'inputEditor', name: 'answer_text', label: 'متن پاسخ', placeholder: ' ', col: 'col-12' },
+        { type: 'file', name: 'answer_attachment', label: 'فایل جامع پاسخ سوالات', placeholder: ' ', col: 'col-12' },
+        { type: 'hidden', name: 'answer_book', value: this.answerBook.id },
+        { type: 'hidden', name: 'test_set_question', value: null }
+      ]
+    }
+  },
+  computed: {
+    hasNextQuestion () {
+      return this.answerBook.test_info.test_set_info.test_set_questions.length > this.questionNumber
+    },
+    hasPrevQuestion () {
+      return this.questionNumber > 1
+    },
+    isLastQuestion () {
+      return this.answerBook.test_info.test_set_info.test_set_questions.length === this.questionNumber
+    },
+    question () {
+      return this.answerBook.test_info.test_set_info.test_set_questions[this.questionNumber - 1]
+    }
+  },
+  mounted () {
+    this.setInputs()
+    this.mounted = true
+    if (this.answerBook.test_info.test_set_info.test_set_questions.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')
+      }
+
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btnFinal', 'atClick', () => { this.sendAnswer('final') })
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btnNext', 'atClick', () => { this.sendAnswer('next') })
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btnPrev', 'atClick', () => { this.sendAnswer('prev') })
+      FormBuilderAssist.setAttributeByName(this.inputs, 'test_set_question', 'value', this.question?.id)
+    },
+    sendAnswer (actionType) {
+      const answerText = FormBuilderAssist.getInputsByName(this.inputs, 'answer_text').value
+      if (!answerText) {
+        this.$q.notify({
+          type: 'negative',
+          message: 'لطفا متن پاسخ را کامل کنید.'
+        })
+        return
+      }
+
+      this.$emit('sending')
+      this.$refs.entityCreate.createEntity(false)
+        .then(() => {
+          this.$emit('sentSuccess', actionType)
+          if (actionType === 'next') {
+            this.goToQuestion(this.questionNumber + 1)
+          }
+          if (actionType === 'prev') {
+            this.goToQuestion(this.questionNumber - 1)
+          }
+          if (actionType === 'final') {
+            this.$q.notify({
+              message: 'پاسخ شما با موفقیت ارسال شد.',
+              type: 'positive'
+            })
+            // this.$router.push({ name: 'UserPanel.Profile.AllClassrooms' })
+            this.goToQuestion('complete')
+          }
+        })
+        .catch(() => {
+          this.$emit('sentSuccess', actionType)
+        })
+    },
+    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;
+  }
+}
+</style>

+ 152 - 76
src/components/Widgets/Test/TestShow/TestShow.vue

@@ -1,86 +1,171 @@
 <template>
-  <div class="AdminTestShow"
+  <div class="TestShow"
        :style="localOptions.style">
-    <div class="flex justify-end">
-      <q-btn flat
-             color="grey"
-             @click="$router.go(-1)">
-        بازگشت
-        >
-      </q-btn>
+    <q-card v-if="!answerBook.loading && answerBook.id">
+      <q-card-section class="flex justify-between">
+        <div>{{ answerBook.test_info.title }}</div>
+      </q-card-section>
+      <q-separator />
+      <q-banner>
+        <div class="row">
+          <div class="col-md-4 col-12">
+            {{ toShamsi(answerBook.attending_start_time) }}
+          </div>
+          <div class="col-md-4 col-12">
+            {{ answerBook.test_info.test_set_info.test_set_questions.length }}
+            سوال
+          </div>
+          <div class="col-md-4 col-12">
+            {{ answerBook.duration }}
+            دقیقه
+          </div>
+        </div>
+      </q-banner>
+      <q-list separator>
+        <q-item v-for="(question, questionIndex) in answerBook.test_info.test_set_info.test_set_questions"
+                :key="questionIndex"
+                clickable>
+          <q-item-section v-html="question.question_info.text" />
+          <q-item-section side>
+            <q-badge>
+              {{ question.mark }}
+              نمره
+            </q-badge>
+          </q-item-section>
+        </q-item>
+      </q-list>
+    </q-card>
+    <div v-else>
+      کمی صبر کنید...
     </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 Assist from 'src/assets/js/Assist.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'
-
-const BtnControlComp = shallowRef(BtnControl)
-const UsersOfTestSelectorComp = shallowRef(UsersOfTestSelector)
+import { AnswerBook } from 'src/models/AnswerBook.js'
 
 export default {
-  name: 'AdminTestShow',
-  components: { EntityEdit },
+  name: 'TestShow',
   mixins: [mixinWidget],
   data () {
     return {
       mounted: false,
-      entityLoading: false,
-      api: APIGateway.test.APIAdresses.byId(this.$route.params.id),
-      entityIdKey: 'id',
-      entityParamKey: 'id',
-      inputs: [
-        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-12' },
-        { type: 'inputEditor', name: 'detail', responseKey: 'detail', label: 'توضیحات آزمون', col: 'col-12' },
-        { type: 'dateTime', name: 'start_time', responseKey: 'start_time', label: 'زمان شروع آزمون', placeholder: ' ', col: 'col-md-6 col-12' },
-        { type: 'dateTime', name: 'end_time', responseKey: 'end_time', label: 'زمان پایان آزمون', placeholder: ' ', col: 'col-md-6 col-12' },
-        { type: 'input', name: 'duration_deadline', responseKey: 'duration_deadline', label: 'مدت زمان پاسخ دهی (دقیقه)', placeholder: ' ', col: 'col-md-6 col-12' },
-        { type: 'dateTime', name: 'announcement_time', responseKey: 'announcement_time', label: 'زمان اعلام نتایج', placeholder: ' ', col: 'col-md-6 col-12' },
-        { type: 'dateTime', name: 'objection_start_time', responseKey: 'objection_start_time', label: 'زمان شروع اعتراض', placeholder: ' ', col: 'col-md-6 col-12' },
-        { type: 'dateTime', name: 'objection_end_time', responseKey: 'objection_end_time', label: 'زمان پایان اعتراض', placeholder: ' ', col: 'col-md-6 col-12' },
-        { type: 'hidden', name: 'test_set', responseKey: 'test_set', value: this.$route.params.test_set_id },
-        { type: 'hidden', name: 'classroom', responseKey: 'classroom', value: this.$route.params.classroom_id },
-        { type: UsersOfTestSelectorComp, name: 'examinees', responseKey: 'examinees', classroomId: this.$route.params.classroom_id, col: 'col-12' },
-        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'تعیین جزییات', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' }
-      ]
+      participateType: null,
+      participateTypeDialog: false,
+      answerBook: new AnswerBook(),
+      remainingTime: 0,
+      timerInterval: null
     }
   },
-  mounted() {
-    this.setActionBtn()
+  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: {
-    setActionBtn () {
-      FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.edit)
+    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 } })
     },
-    edit() {
-      this.entityLoading = true
-      this.$refs.entityEdit.editEntity()
-        .then(() => {
-          this.$refs.entityEdit.getData()
-          this.entityLoading = false
+    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
+    },
+    startTimer () {
+      this.timerInterval = setInterval(() => {
+        if (this.remainingTime <= 0) {
+          this.stopTimer()
+        } else {
+          this.remainingTime--
+        }
+      }, 1000)
+    },
+    stopTimer () {
+      if (!this.timerInterval) {
+        return
+      }
+
+      clearInterval(this.timerInterval)
+    },
+    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.server_time = '2023-12-15T23:57:46.909211'
+          this.answerBook.attending_start_time = '2023-12-15T23:27:46.909211'
+          this.remainingTime = this.getRemainingTimeInSeconds(this.answerBook.server_time, this.answerBook.attending_start_time, this.answerBook.duration)
+          this.startTimer()
         })
         .catch(() => {
-          this.entityLoading = false
+          this.answerBook.loading = false
+          this.backToClassList()
         })
     }
   }
@@ -88,23 +173,14 @@ export default {
 </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;
+.TestShow {
+  :deep(.q-banner) {
+    background: #FCF9F4 0 0 no-repeat padding-box;
+    padding: 20px;
+    .q-banner__content {
+      font-size: 16px;
+      font-weight: normal;
     }
   }
-  :deep(.form) {
-    padding: 24px;
-  }
 }
 </style>

+ 112 - 0
src/models/AnswerSheet.js

@@ -0,0 +1,112 @@
+import { User } from 'src/models/User.js'
+import { Test } from 'src/models/Test.js'
+import { Collection, Model } from 'js-abstract-model'
+
+class AnswerSheet 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: '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: 'registration' },
+      { key: 'registration_info' },
+      { key: 'server_time' },
+      { key: 'attending_start_time' },
+      { key: 'status' },
+      {
+        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: '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 AnswerSheetList extends Collection {
+  model () {
+    return AnswerSheet
+  }
+}
+
+export { AnswerSheet, AnswerSheetList }

+ 24 - 7
src/pages/User/Profile/classroomInfo.vue

@@ -146,10 +146,25 @@
               {{ inputData.rowNumber }}
             </template>
             <template v-else-if="inputData.col.name === 'action'">
-              <q-btn size="md"
+              <q-btn v-if="inputData.props.row.is_enabled_attending"
+                     size="md"
                      color="primary"
-                     label="جزییات"
-                     :to="{name: 'UserPanel.Profile.Test', params: {id: inputData.props.row.id}}" />
+                     label="شرکت در آزمون"
+                     :to="{name: 'UserPanel.Test.AnswerBook.Confirmation', params: {test_id: inputData.props.row.test, answer_book_id: inputData.props.row.id}}" />
+              <q-btn v-if="inputData.props.row.is_enabled_continuing"
+                     size="md"
+                     color="primary"
+                     label="ادامه آزمون"
+                     :to="{name: 'UserPanel.Test.AnswerBook.Confirmation', params: {test_id: inputData.props.row.test, answer_book_id: inputData.props.row.id}}" />
+              <q-btn v-if="inputData.props.row.is_enabled_objecting"
+                     size="md"
+                     color="primary"
+                     label="ثبت اعتراض" />
+              <q-btn v-if="inputData.props.row.is_enabled_viewing"
+                     size="md"
+                     color="primary"
+                     label="مشاهده"
+                     :to="{name: 'UserPanel.Test.AnswerBook.Show', params: {test_id: inputData.props.row.test, answer_book_id: inputData.props.row.id}}" />
             </template>
             <template v-else>
               {{ inputData.col.value }}
@@ -165,7 +180,7 @@
                   <q-btn size="md"
                          color="primary"
                          label="جزییات"
-                         :to="{name: 'UserPanel.Profile.SessionInfo', params: {id: row.id}}" />
+                         :to="{name: 'UserPanel.Test.AnswerBook.Confirmation', params: {test_id: row.test, answer_book_id: row.id}}" />
                 </template>
               </template>
             </entity-index-grid-item>
@@ -238,11 +253,13 @@
 
 <script>
 import { EntityIndex } from 'quasar-crud'
+import { Team } from 'src/models/Team.js'
 import Enums from 'src/assets/Enums/Enums.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 EntityIndexGridItem from 'src/components/EntityIndexGridItem.vue'
 import { Registration, RegistrationList } from 'src/models/Registration.js'
@@ -250,8 +267,6 @@ import Breadcrumbs from 'src/components/Widgets/Breadcrumbs/Breadcrumbs.vue'
 import { ProjectAttendanceSheets } from 'src/models/ProjectAttendanceSheets.js'
 import { SessionAttendanceSheets } from 'src/models/SessionAttendanceSheets.js'
 import ShowClassroomInfo from 'src/components/Widgets/Other/ShowClassroomInfo/ShowClassroomInfo.vue'
-import { Team } from 'src/models/Team.js'
-import { AnswerBook } from 'src/models/AnswerBook.js'
 
 export default {
   name: 'UserPanel.Profile.ClassroomInfo',
@@ -534,7 +549,8 @@ export default {
       },
 
       testListInputs: [
-        { type: 'hidden', name: 'classroom', value: classroomId }
+        { type: 'hidden', name: 'test__classroom', value: classroomId },
+        { type: 'hidden', name: 'owner', value: null }
       ],
       testListApi: APIGateway.answerBook.APIAdresses.base,
       testListTable: {
@@ -640,6 +656,7 @@ export default {
   mounted () {
     this.getRegistrationInfo()
     this.mounted = true
+    FormBuilderAssist.setAttributeByName(this.testListInputs, 'owner', 'value', this.user.id)
   },
   methods: {
     getTeamJoinStatusLabel (team) {

+ 24 - 0
src/pages/User/Test/Confirmation.vue

@@ -0,0 +1,24 @@
+<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.Test.AnswerBook.Confirmation',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'TestConfirmation' }] }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '30px', style: { maxWidth: '1200px', width: '1200px' } } }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+    this.pageBuilderLoading = false
+  }
+}
+</script>

+ 24 - 0
src/pages/User/Test/ParticipateAllQuestions.vue

@@ -0,0 +1,24 @@
+<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.Test.AnswerBook.Participate.AllQuestions',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'TestParticipation' }] }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '30px', style: { maxWidth: '1200px', width: '1200px' } } }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+    this.pageBuilderLoading = false
+  }
+}
+</script>

+ 24 - 0
src/pages/User/Test/ParticipateSingleQuestion.vue

@@ -0,0 +1,24 @@
+<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.Test.AnswerBook.Participate.SingleQuestion',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'TestParticipation' }] }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '30px', style: { maxWidth: '1200px', width: '1200px' } } }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+    this.pageBuilderLoading = false
+  }
+}
+</script>

+ 24 - 0
src/pages/User/Test/SendObjection.vue

@@ -0,0 +1,24 @@
+<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: 'AdminPanel.Package.Show',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'Breadcrumbs', options: { style: { marginTop: '29px', marginBottom: '49px' } } }, { name: 'TicketShow' }], options: { style: { paddingTop: '', paddingBottom: '100px' } } }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '10px', alignment: { justifyContent: { global: null, xl: null, lg: null, md: null, sm: null, xs: null }, alignItems: { global: null, xl: null, lg: null, md: null, sm: null, xs: null }, alignContent: { global: null, xl: null, lg: null, md: null, sm: null, xs: null } }, style: { maxWidth: '1200px', width: '1200px' } } }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+    this.pageBuilderLoading = false
+  }
+}
+</script>

+ 24 - 0
src/pages/User/Test/Show.vue

@@ -0,0 +1,24 @@
+<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: 'AdminPanel.Package.Show',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'TestShow' }] }] }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+    this.pageBuilderLoading = false
+  }
+}
+</script>

+ 12 - 0
src/router/routes.js

@@ -324,6 +324,18 @@ const routes = [
                   { name: 'UserPanel.Leader.Teams.Index', path: 'teams', component: () => import('pages/Admin/Leader/MyTeams.vue') },
                   { name: 'UserPanel.Leader.Teams.Show', path: 'teams/:team_id', component: () => import('pages/Admin/Leader/ShowTeam.vue') }
                 ]
+              },
+              {
+                name: 'UserPanel.Test',
+                path: 'test/:test_id',
+                component: () => import('layouts/bareLayout.vue'),
+                children: [
+                  { name: 'UserPanel.Test.AnswerBook.Confirmation', path: 'answer_book/:answer_book_id/confirmation', component: () => import('src/pages/User/Test/Confirmation.vue') },
+                  { name: 'UserPanel.Test.AnswerBook.Participate.AllQuestions', path: 'answer_book/:answer_book_id/participate/all_questions', component: () => import('src/pages/User/Test/ParticipateAllQuestions.vue') },
+                  { name: 'UserPanel.Test.AnswerBook.Participate.SingleQuestion', path: 'answer_book/:answer_book_id/participate/question/:question_number', component: () => import('src/pages/User/Test/ParticipateSingleQuestion.vue') },
+                  { name: 'UserPanel.Test.AnswerBook.Show', path: 'answer_book/:answer_book_id/show', component: () => import('src/pages/User/Test/Show.vue') },
+                  { name: 'UserPanel.Test.AnswerBook.SendObjection', path: 'answer_book/:answer_book_id/sendObjection', component: () => import('src/pages/User/Test/SendObjection.vue') }
+                ]
               }
             ]
           },