AdminAnswerBookShow.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  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="answerBook.status === 'GRADING'">
  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(this.user, 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(this.user, 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. export default {
  258. name: 'AdminAnswerBookShow',
  259. components: { EntityShow, EntityEdit },
  260. mixins: [mixinWidget, mixinAuth],
  261. data () {
  262. return {
  263. mounted: false,
  264. replyText: null,
  265. entityLoading: true,
  266. graderDescriptionLoading: false,
  267. overallAnswerInput: [
  268. // { type: 'input', name: 'overall_answer_text', responseKey: 'overall_answer_text', label: 'متن پاسخ جامع', placeholder: ' ', inputType: 'textarea', col: 'col-12' },
  269. { type: 'file', name: 'overall_answer_attachment', responseKey: 'overall_answer_attachment', label: 'فایل پیوست جامع', placeholder: ' ', col: 'col-12' }
  270. ],
  271. questionInput: [
  272. { type: 'input', name: 'answer_text', responseKey: 'answer_text', label: 'متن پاسخ', placeholder: ' ', inputType: 'textarea', col: 'col-12' },
  273. { type: 'file', name: 'answer_attachment', responseKey: 'answer_attachment', label: 'فایل پیوست', placeholder: ' ', col: 'col-12' }
  274. ],
  275. questionInputs: [],
  276. scores: [],
  277. objectionInputs: [
  278. { type: 'input', name: 'objection_result', responseKey: 'objection_result', label: 'متن پاسخ', placeholder: ' ', inputType: 'textarea', col: 'col-12' }
  279. ],
  280. objectionApi: APIGateway.answerBook.APIAdresses.submitObjectionResult(this.$route.params.answer_book_id),
  281. answerBook: new AnswerBook()
  282. }
  283. },
  284. computed: {
  285. totalScore () {
  286. let totalScore = 0
  287. this.scores.forEach(item => {
  288. if (item.score) {
  289. totalScore += parseFloat(item.score)
  290. }
  291. })
  292. return totalScore
  293. }
  294. },
  295. mounted () {
  296. this.loadAuthData()
  297. this.getAnswerBook()
  298. this.mounted = true
  299. },
  300. methods: {
  301. scrollToObjectionBox() {
  302. this.$nextTick(() => {
  303. if (this.$refs.objectionBox && this.$refs.objectionBox.$el) {
  304. this.$refs.objectionBox.$el.scrollIntoView({ behavior: 'smooth', block: 'end' })
  305. }
  306. })
  307. },
  308. closeTicket () {
  309. this.$q.dialog({
  310. title: 'توجه',
  311. message: 'آیا از بستن تیکت اطمینان دارید؟',
  312. cancel: true,
  313. persistent: true
  314. }).onOk(() => {
  315. this.entityLoading = true
  316. APIGateway.ticket.close(this.$route.params.id)
  317. .then(() => {
  318. this.$refs.entityEdit.getData()
  319. })
  320. .catch(() => {
  321. this.entityLoading = false
  322. })
  323. })
  324. },
  325. miladiToShamsi (miladi) {
  326. return Assist.miladiToShamsi(miladi)
  327. },
  328. isUserMessage (authenticatedUser, message) {
  329. return authenticatedUser.id === message.creator_info.id
  330. },
  331. removeMessage (replyMessageId) {
  332. this.$q.dialog({
  333. title: 'توجه',
  334. message: 'آیا از حذف پیام اطمینان دارید؟',
  335. cancel: true,
  336. persistent: true
  337. }).onOk(() => {
  338. this.entityLoading = true
  339. APIGateway.ticket.deleteReplyMessage(replyMessageId)
  340. .then(() => {
  341. this.getAnswerBook()
  342. })
  343. .catch(() => {
  344. this.entityLoading = false
  345. })
  346. })
  347. },
  348. sendReply() {
  349. this.entityLoading = true
  350. const body = this.replyText.replace(/\n/g, '<br>\n')
  351. APIGateway.ticket.reply({
  352. id: this.answerBook.ticket,
  353. body
  354. })
  355. .then(() => {
  356. this.replyText = null
  357. this.getAnswerBook()
  358. })
  359. .catch(() => {
  360. this.getAnswerBook()
  361. })
  362. },
  363. sendObjection () {
  364. this.answerBook.loading = true
  365. this.$refs.objectionEntityEdit.editEntity(false)
  366. .then(() => {
  367. this.getAnswerBook()
  368. })
  369. .catch(() => {
  370. this.getAnswerBook()
  371. })
  372. },
  373. toShamsi (miladi) {
  374. return Assist.miladiToShamsi(miladi)
  375. },
  376. loadAnswerBook (answerBook) {
  377. this.answerBook = new AnswerBook(answerBook)
  378. this.answerBook.answer_sheet_info.list.forEach((answerSheetItem) => {
  379. this.questionInputs.push(this.questionInput)
  380. this.scores.push({
  381. answerSheetId: answerSheetItem.id,
  382. score: answerSheetItem.score,
  383. loading: false
  384. })
  385. })
  386. },
  387. setGraderToMe () {
  388. this.answerBook.loading = true
  389. APIGateway.answerBook.setGraderToMe(this.$route.params.answer_book_id)
  390. .then((answerBook) => {
  391. this.loadAnswerBook(answerBook)
  392. this.answerBook.loading = false
  393. })
  394. .catch(() => {
  395. this.getAnswerBook()
  396. this.answerBook.loading = false
  397. })
  398. },
  399. submitGraderDescription () {
  400. this.graderDescriptionLoading = true
  401. APIGateway.answerBook.submitGraderDescription(this.answerBook.id, this.answerBook.grader_description)
  402. .then(() => {
  403. this.graderDescriptionLoading = false
  404. this.getAnswerBook()
  405. })
  406. .catch(() => {
  407. this.graderDescriptionLoading = false
  408. })
  409. },
  410. submitScore (index) {
  411. this.scores[index].loading = true
  412. APIGateway.answerSheet.submitScore(this.scores[index].answerSheetId, this.scores[index].score)
  413. .then(() => {
  414. this.scores[index].loading = false
  415. this.getAnswerBook()
  416. })
  417. .catch(() => {
  418. this.scores[index].loading = false
  419. })
  420. },
  421. getAnswerBook () {
  422. this.answerBook.loading = true
  423. this.entityLoading = true
  424. APIGateway.answerBook.get(this.$route.params.answer_book_id)
  425. .then((answerBook) => {
  426. this.loadAnswerBook(answerBook)
  427. this.answerBook.loading = false
  428. this.entityLoading = false
  429. })
  430. .catch(() => {
  431. this.answerBook.loading = false
  432. this.entityLoading = false
  433. })
  434. },
  435. confirmScores () {
  436. this.answerBook.loading = true
  437. APIGateway.answerBook.confirmScores(this.$route.params.answer_book_id)
  438. .then(() => {
  439. this.getAnswerBook()
  440. this.answerBook.loading = false
  441. })
  442. .catch(() => {
  443. this.getAnswerBook()
  444. this.answerBook.loading = false
  445. })
  446. },
  447. confirmUngradable () {
  448. this.answerBook.loading = true
  449. APIGateway.answerBook.confirmUngradable(this.$route.params.answer_book_id)
  450. .then(() => {
  451. this.getAnswerBook()
  452. this.answerBook.loading = false
  453. })
  454. .catch(() => {
  455. this.getAnswerBook()
  456. this.answerBook.loading = false
  457. })
  458. }
  459. }
  460. }
  461. </script>
  462. <style scoped lang="scss">
  463. .AdminAnswerBookShow {
  464. :deep(.q-banner) {
  465. background: #FCF9F4 0 0 no-repeat padding-box;
  466. padding: 20px;
  467. .q-banner__content {
  468. font-size: 16px;
  469. font-weight: normal;
  470. }
  471. }
  472. :deep(.q-list) {
  473. .q-item {
  474. .input-grate-question {
  475. .q-input {
  476. .q-field__inner .q-field__control {
  477. height: 40px;
  478. min-height: 40px;
  479. }
  480. }
  481. }
  482. .q-badge {
  483. background: #ACBCAE 0 0 no-repeat padding-box;
  484. border-radius: 18px;
  485. height: 32px;
  486. padding: 4px 16px;
  487. }
  488. }
  489. }
  490. }
  491. </style>