AdminAnswerBookShow.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. <template>
  2. <div class="AdminAnswerBookShow"
  3. :style="localOptions.style">
  4. <q-card v-if="!answerBook.loading && answerBook.id">
  5. <q-card-section class="flex justify-between">
  6. <div>{{ answerBook.test_info.title }}</div>
  7. <q-btn v-if="!answerBook.loading && answerBook.ticket"
  8. outline
  9. color="primary"
  10. class="btn-need-installment"
  11. @click="scrollToObjectionBox">
  12. مشاهده اعتراض
  13. </q-btn>
  14. </q-card-section>
  15. <q-separator />
  16. <q-banner>
  17. <div class="row">
  18. <div class="col-md-6 col-12">
  19. وضعیت آزمون:
  20. {{ answerBook.status_info.label }}
  21. </div>
  22. <div v-if="answerBook.grader_info"
  23. class="col-md-6 col-12">
  24. مصحح:
  25. {{ answerBook.grader_info.firstname }}
  26. {{ answerBook.grader_info.lastname }}
  27. (کدملی: {{ answerBook.grader_info.national_code }})
  28. </div>
  29. </div>
  30. </q-banner>
  31. <q-separator />
  32. <q-banner>
  33. <div class="row">
  34. <div class="col-md-4 col-12">
  35. زمان شروع:
  36. {{ toShamsi(answerBook.attending_start_time) }}
  37. </div>
  38. <div class="col-md-4 col-12">
  39. {{ answerBook.owner_info.firstname }}
  40. {{ answerBook.owner_info.lastname }}
  41. (کدملی: {{ answerBook.owner_info.national_code }})
  42. </div>
  43. <div class="col-md-4 col-12">
  44. نمره نهایی
  45. <template v-if="answerBook.status === 'GRADING'">
  46. {{ totalScore }}
  47. </template>
  48. <template v-else>
  49. {{ answerBook.total_score }}
  50. </template>
  51. </div>
  52. </div>
  53. </q-banner>
  54. <q-list separator>
  55. <q-item v-for="(answerSheet, answerSheetIndex) in answerBook.answer_sheet_info.list"
  56. :key="answerSheetIndex"
  57. clickable>
  58. <div class="row full-width q-col-gutter-md">
  59. <div class="col-md-10 col-12">
  60. <div v-html="'<span>' + (answerSheetIndex + 1) + ' - </span>' + answerSheet.test_question_info.question_info.text" />
  61. <q-card class="q-mt-sm">
  62. <q-card-section>
  63. <div>
  64. پاسخ صحیح:
  65. </div>
  66. <div v-html="answerSheet.test_question_info.question_info.correct_answer" />
  67. </q-card-section>
  68. </q-card>
  69. <q-card class="q-mt-sm">
  70. <q-card-section>
  71. <div>
  72. متن پاسخ کاربر:
  73. </div>
  74. <div>
  75. {{ answerSheet.answer_text }}
  76. </div>
  77. <div v-if="answerSheet.answer_attachment">
  78. <q-separator class="q-my-md" />
  79. <q-btn outline
  80. color="primary"
  81. icon="attachment"
  82. label="فایل پیوست کاربر"
  83. :href="answerSheet.answer_attachment"
  84. target="_blank" />
  85. </div>
  86. </q-card-section>
  87. </q-card>
  88. </div>
  89. <div v-if="$route.query.classroom_type !== 'EVENT'"
  90. class="col-md-2 col-12">
  91. <q-badge v-if="answerSheet.score !== null">
  92. {{ answerSheet.score }}
  93. از
  94. {{ answerSheet.test_question_info.mark }}
  95. نمره
  96. </q-badge>
  97. <q-badge v-else>
  98. {{ answerSheet.test_question_info.question_info.mark }}
  99. نمره
  100. </q-badge>
  101. <div class="q-mt-md input-grate-question">
  102. <q-input v-model="scores[answerSheetIndex].score"
  103. :loading="scores[answerSheetIndex].loading"
  104. dense>
  105. <template v-slot:append>
  106. <q-btn color="primary"
  107. label="ثبت نمره"
  108. outline
  109. :loading="scores[answerSheetIndex].loading"
  110. @click="submitScore(answerSheetIndex)" />
  111. </template>
  112. </q-input>
  113. </div>
  114. </div>
  115. </div>
  116. </q-item>
  117. </q-list>
  118. <q-separator class="q-my-lg" />
  119. <div class="q-pa-lg">
  120. <entity-show v-if="mounted"
  121. ref="entityShow"
  122. v-model:value="overallAnswerInput"
  123. :loaded-data="answerBook"
  124. :show-close-button="false"
  125. :show-edit-button="false"
  126. :show-expand-button="false"
  127. :show-save-button="false"
  128. :show-index-button="false"
  129. :show-reload-button="false"
  130. :default-layout="false" />
  131. </div>
  132. <template v-if="answerBook.objection_request">
  133. <div>
  134. متن اعتراض:
  135. </div>
  136. <div v-html="answerBook.objection_request" />
  137. </template>
  138. <template v-if="answerBook.objection_request && !answerBook.objection_result">
  139. <q-separator class="q-my-xl" />
  140. <q-card-section>
  141. <entity-edit v-if="mounted"
  142. ref="objectionEntityEdit"
  143. v-model:value="objectionInputs"
  144. title="پاسخ به اعتراض"
  145. :api="objectionApi"
  146. :loaded-data="{}"
  147. :show-close-button="false"
  148. :show-edit-button="false"
  149. :show-expand-button="false"
  150. :show-save-button="false"
  151. :show-reload-button="false">
  152. <template #after-form-builder>
  153. <div class="flex justify-end q-mt-md">
  154. <q-btn color="primary"
  155. label="ثبت اعتراض"
  156. @click="sendObjection" />
  157. </div>
  158. </template>
  159. </entity-edit>
  160. </q-card-section>
  161. </template>
  162. <template v-else-if="answerBook.objection_result">
  163. <div>
  164. نتیجه اعتراض:
  165. </div>
  166. <div v-html="answerBook.objection_result" />
  167. </template>
  168. <q-banner>
  169. <q-input v-model="answerBook.grader_description"
  170. type="textarea"
  171. label="توضیحات مصحح"
  172. :loading="graderDescriptionLoading" />
  173. <div class="row justify-end q-py-md">
  174. <q-btn color="primary"
  175. label="ثبت توضیحات مصحح"
  176. outline
  177. :loading="graderDescriptionLoading"
  178. @click="submitGraderDescription" />
  179. </div>
  180. </q-banner>
  181. <q-card-actions v-if="!['DELETED', 'GRADED', 'FINALIZED'].includes(answerBook.status)">
  182. <q-btn color="primary"
  183. class="q-mr-lg"
  184. @click="confirmScores">
  185. تایید نهایی نمرات
  186. </q-btn>
  187. <q-btn color="red"
  188. @click="confirmUngradable">
  189. نیاز به رفع مشکل
  190. </q-btn>
  191. </q-card-actions>
  192. </q-card>
  193. <div v-else>
  194. کمی صبر کنید...
  195. </div>
  196. </div>
  197. <q-card v-if="mounted && !answerBook.loading && answerBook.id && answerBook.ticket"
  198. ref="objectionBox"
  199. class="q-mt-md">
  200. <q-card-section>
  201. <div class="row justify-center">
  202. <div class="full-width">
  203. <q-skeleton v-if="entityLoading"
  204. type="rect"
  205. width="100%"
  206. height="200px" />
  207. <template v-else>
  208. {{ answerBook.ticket_info.body }}
  209. <q-chat-message v-for="message in answerBook.ticket_info.replies_info.list"
  210. :key="message.id"
  211. :avatar="message.creator_info?.picture ? message.creator_info.picture : 'assets/images/web/sample-avatar.svg'"
  212. :name="message.creator_info?.firstname + ' ' + message.creator_info?.lastname"
  213. text-html
  214. :text="[message.body]"
  215. :sent="!isUserMessage(authenticatedUser, message)">
  216. <template v-slot:stamp>
  217. <div>
  218. {{ miladiToShamsi(message.creation_time) }}
  219. </div>
  220. <q-btn icon="clear"
  221. round
  222. color="red"
  223. flat
  224. size="xs"
  225. @click="removeMessage(message.id)" />
  226. <q-icon v-if="isUserMessage(authenticatedUser, message)"
  227. :name="message.read ? 'done_all' : 'done'" />
  228. </template>
  229. </q-chat-message>
  230. <div>
  231. <q-input v-model="replyText"
  232. bottom-slots
  233. autogrow
  234. :loading="entityLoading">
  235. <template v-slot:after>
  236. <q-btn round
  237. dense
  238. flat
  239. icon="send"
  240. :loading="entityLoading"
  241. @click="sendReply" />
  242. </template>
  243. </q-input>
  244. </div>
  245. </template>
  246. </div>
  247. </div>
  248. </q-card-section>
  249. </q-card>
  250. </template>
  251. <script>
  252. import Assist from 'src/assets/js/Assist.js'
  253. import { APIGateway } from 'src/api/APIGateway.js'
  254. import { EntityShow, EntityEdit } from 'quasar-crud'
  255. import { AnswerBook } from 'src/models/AnswerBook.js'
  256. import { mixinWidget, mixinAuth } from 'src/mixin/Mixins.js'
  257. import { User } from 'src/models/User.js'
  258. export default {
  259. name: 'AdminAnswerBookShow',
  260. components: { EntityShow, EntityEdit },
  261. mixins: [mixinWidget, mixinAuth],
  262. data () {
  263. return {
  264. mounted: false,
  265. replyText: null,
  266. entityLoading: true,
  267. authenticatedUser: new User(),
  268. graderDescriptionLoading: false,
  269. overallAnswerInput: [
  270. // { type: 'input', name: 'overall_answer_text', responseKey: 'overall_answer_text', label: 'متن پاسخ جامع', placeholder: ' ', inputType: 'textarea', col: 'col-12' },
  271. { type: 'file', name: 'overall_answer_attachment', responseKey: 'overall_answer_attachment', label: 'فایل پیوست جامع', placeholder: ' ', col: 'col-12' }
  272. ],
  273. questionInput: [
  274. { type: 'input', name: 'answer_text', responseKey: 'answer_text', label: 'متن پاسخ', placeholder: ' ', inputType: 'textarea', col: 'col-12' },
  275. { type: 'file', name: 'answer_attachment', responseKey: 'answer_attachment', label: 'فایل پیوست', placeholder: ' ', col: 'col-12' }
  276. ],
  277. questionInputs: [],
  278. scores: [],
  279. objectionInputs: [
  280. { type: 'input', name: 'objection_result', responseKey: 'objection_result', label: 'متن پاسخ', placeholder: ' ', inputType: 'textarea', col: 'col-12' }
  281. ],
  282. objectionApi: APIGateway.answerBook.APIAdresses.submitObjectionResult(this.$route.params.answer_book_id),
  283. answerBook: new AnswerBook()
  284. }
  285. },
  286. computed: {
  287. totalScore () {
  288. let totalScore = 0
  289. this.scores.forEach(item => {
  290. if (item.score) {
  291. totalScore += parseFloat(item.score)
  292. }
  293. })
  294. return totalScore
  295. }
  296. },
  297. mounted () {
  298. this.loadAuthData()
  299. this.getAnswerBook()
  300. this.mounted = true
  301. },
  302. methods: {
  303. scrollToObjectionBox() {
  304. this.$nextTick(() => {
  305. if (this.$refs.objectionBox && this.$refs.objectionBox.$el) {
  306. this.$refs.objectionBox.$el.scrollIntoView({ behavior: 'smooth', block: 'end' })
  307. }
  308. })
  309. },
  310. loadAuthData () {
  311. this.authenticatedUser = this.$store.getters['Auth/user']
  312. },
  313. closeTicket () {
  314. this.$q.dialog({
  315. title: 'توجه',
  316. message: 'آیا از بستن تیکت اطمینان دارید؟',
  317. cancel: true,
  318. persistent: true
  319. }).onOk(() => {
  320. this.entityLoading = true
  321. APIGateway.ticket.close(this.$route.params.id)
  322. .then(() => {
  323. this.$refs.entityEdit.getData()
  324. })
  325. .catch(() => {
  326. this.entityLoading = false
  327. })
  328. })
  329. },
  330. miladiToShamsi (miladi) {
  331. return Assist.miladiToShamsi(miladi)
  332. },
  333. isUserMessage (authenticatedUser, message) {
  334. return authenticatedUser.id === message.creator_info.id
  335. },
  336. removeMessage (replyMessageId) {
  337. this.$q.dialog({
  338. title: 'توجه',
  339. message: 'آیا از حذف پیام اطمینان دارید؟',
  340. cancel: true,
  341. persistent: true
  342. }).onOk(() => {
  343. this.entityLoading = true
  344. APIGateway.ticket.deleteReplyMessage(replyMessageId)
  345. .then(() => {
  346. this.getAnswerBook()
  347. })
  348. .catch(() => {
  349. this.entityLoading = false
  350. })
  351. })
  352. },
  353. sendReply() {
  354. this.entityLoading = true
  355. const body = this.replyText.replace(/\n/g, '<br>\n')
  356. APIGateway.ticket.reply({
  357. id: this.answerBook.ticket,
  358. body
  359. })
  360. .then(() => {
  361. this.replyText = null
  362. this.getAnswerBook()
  363. })
  364. .catch(() => {
  365. this.getAnswerBook()
  366. })
  367. },
  368. sendObjection () {
  369. this.answerBook.loading = true
  370. this.$refs.objectionEntityEdit.editEntity(false)
  371. .then(() => {
  372. this.getAnswerBook()
  373. })
  374. .catch(() => {
  375. this.getAnswerBook()
  376. })
  377. },
  378. toShamsi (miladi) {
  379. return Assist.miladiToShamsi(miladi)
  380. },
  381. loadAnswerBook (answerBook) {
  382. this.answerBook = new AnswerBook(answerBook)
  383. this.answerBook.answer_sheet_info.list.forEach((answerSheetItem) => {
  384. this.questionInputs.push(this.questionInput)
  385. this.scores.push({
  386. answerSheetId: answerSheetItem.id,
  387. score: answerSheetItem.score,
  388. loading: false
  389. })
  390. })
  391. },
  392. setGraderToMe () {
  393. this.answerBook.loading = true
  394. APIGateway.answerBook.setGraderToMe(this.$route.params.answer_book_id)
  395. .then((answerBook) => {
  396. this.loadAnswerBook(answerBook)
  397. this.answerBook.loading = false
  398. })
  399. .catch(() => {
  400. this.getAnswerBook()
  401. this.answerBook.loading = false
  402. })
  403. },
  404. submitGraderDescription () {
  405. this.graderDescriptionLoading = true
  406. APIGateway.answerBook.submitGraderDescription(this.answerBook.id, this.answerBook.grader_description)
  407. .then(() => {
  408. this.graderDescriptionLoading = false
  409. this.getAnswerBook()
  410. })
  411. .catch(() => {
  412. this.graderDescriptionLoading = false
  413. })
  414. },
  415. submitScore (index) {
  416. this.scores[index].loading = true
  417. APIGateway.answerSheet.submitScore(this.scores[index].answerSheetId, this.scores[index].score)
  418. .then(() => {
  419. this.scores[index].loading = false
  420. this.getAnswerBook()
  421. })
  422. .catch(() => {
  423. this.scores[index].loading = false
  424. })
  425. },
  426. getAnswerBook () {
  427. this.answerBook.loading = true
  428. this.entityLoading = true
  429. APIGateway.answerBook.get(this.$route.params.answer_book_id)
  430. .then((answerBook) => {
  431. this.loadAnswerBook(answerBook)
  432. this.answerBook.loading = false
  433. this.entityLoading = false
  434. })
  435. .catch(() => {
  436. this.answerBook.loading = false
  437. this.entityLoading = false
  438. })
  439. },
  440. confirmScores () {
  441. this.answerBook.loading = true
  442. APIGateway.answerBook.confirmScores(this.$route.params.answer_book_id)
  443. .then(() => {
  444. this.getAnswerBook()
  445. this.answerBook.loading = false
  446. })
  447. .catch(() => {
  448. this.getAnswerBook()
  449. this.answerBook.loading = false
  450. })
  451. },
  452. confirmUngradable () {
  453. this.answerBook.loading = true
  454. APIGateway.answerBook.confirmUngradable(this.$route.params.answer_book_id)
  455. .then(() => {
  456. this.getAnswerBook()
  457. this.answerBook.loading = false
  458. })
  459. .catch(() => {
  460. this.getAnswerBook()
  461. this.answerBook.loading = false
  462. })
  463. }
  464. }
  465. }
  466. </script>
  467. <style scoped lang="scss">
  468. .AdminAnswerBookShow {
  469. :deep(.q-banner) {
  470. background: #FCF9F4 0 0 no-repeat padding-box;
  471. padding: 20px;
  472. .q-banner__content {
  473. font-size: 16px;
  474. font-weight: normal;
  475. }
  476. }
  477. :deep(.q-list) {
  478. .q-item {
  479. .input-grate-question {
  480. .q-input {
  481. .q-field__inner .q-field__control {
  482. height: 40px;
  483. min-height: 40px;
  484. }
  485. }
  486. }
  487. .q-badge {
  488. background: #ACBCAE 0 0 no-repeat padding-box;
  489. border-radius: 18px;
  490. height: 32px;
  491. padding: 4px 16px;
  492. }
  493. }
  494. }
  495. }
  496. </style>