QuestionnaireModal.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import { useCallback } from 'react';
  2. import { useTranslation } from 'next-i18next';
  3. import { Modal, ModalBody } from 'reactstrap';
  4. import { apiv3Put } from '~/client/util/apiv3-client';
  5. import { toastSuccess, toastError } from '~/client/util/toastr';
  6. import { useQuestionnaireModal } from '~/features/questionnaire/client/stores/model';
  7. import { IAnswer } from '~/features/questionnaire/interfaces/answer';
  8. import { StatusType } from '~/features/questionnaire/interfaces/questionnaire-answer-status';
  9. import { IQuestionnaireOrderHasId } from '~/features/questionnaire/interfaces/questionnaire-order';
  10. import { useCurrentUser } from '~/stores/context';
  11. import loggerFactory from '~/utils/logger';
  12. import { GuestQuestionnaireAnswerStatusService } from '../services/guest-questionnaire-answer-status';
  13. import Question from './Question';
  14. const logger = loggerFactory('growi:QuestionnaireModal');
  15. type QuestionnaireModalProps = {
  16. questionnaireOrder: IQuestionnaireOrderHasId
  17. }
  18. const QuestionnaireModal = ({ questionnaireOrder }: QuestionnaireModalProps): JSX.Element => {
  19. const { data: currentUser } = useCurrentUser();
  20. const lang = currentUser?.lang;
  21. const { data: questionnaireModalData, close: closeQuestionnaireModal } = useQuestionnaireModal();
  22. const isOpened = questionnaireModalData?.openedQuestionnaireId === questionnaireOrder._id;
  23. const inputNamePrefix = 'question-';
  24. const { t } = useTranslation(['translation', 'commons']);
  25. const sendAnswer = useCallback(async(answers: IAnswer[]) => {
  26. try {
  27. await apiv3Put('/questionnaire/answer', {
  28. questionnaireOrderId: questionnaireOrder._id,
  29. answers,
  30. });
  31. if (currentUser == null) {
  32. GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.answered);
  33. }
  34. toastSuccess(
  35. <>
  36. <div className="fw-bold">{t('questionnaire.thank_you_for_answering')}</div>
  37. <div className="pt-2">{t('questionnaire.additional_feedback')}</div>
  38. </>,
  39. {
  40. autoClose: 3000,
  41. closeButton: true,
  42. },
  43. );
  44. }
  45. catch (e) {
  46. logger.error(e);
  47. toastError(t('questionnaire.failed_to_send'));
  48. }
  49. }, [questionnaireOrder._id, t, currentUser]);
  50. const submitHandler = useCallback(async(event) => {
  51. event.preventDefault();
  52. const answers: IAnswer[] = questionnaireOrder.questions.map((question) => {
  53. const answerValue = event.target[`${inputNamePrefix + question._id}`].value;
  54. return { question: question._id, value: answerValue };
  55. });
  56. sendAnswer(answers);
  57. const shouldCloseToast = true;
  58. closeQuestionnaireModal(shouldCloseToast);
  59. }, [closeQuestionnaireModal, questionnaireOrder.questions, sendAnswer]);
  60. const denyBtnClickHandler = useCallback(async() => {
  61. try {
  62. apiv3Put('/questionnaire/deny', {
  63. questionnaireOrderId: questionnaireOrder._id,
  64. });
  65. if (currentUser == null) {
  66. GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.denied);
  67. }
  68. toastSuccess(t('questionnaire.denied'));
  69. }
  70. catch (e) {
  71. logger.error(e);
  72. }
  73. const shouldCloseToast = true;
  74. closeQuestionnaireModal(shouldCloseToast);
  75. }, [closeQuestionnaireModal, questionnaireOrder._id, t, currentUser]);
  76. // No showing toasts since not important
  77. const closeBtnClickHandler = useCallback(async(shouldCloseToast: boolean) => {
  78. closeQuestionnaireModal(shouldCloseToast);
  79. try {
  80. await apiv3Put('/questionnaire/skip', {
  81. questionnaireOrderId: questionnaireOrder._id,
  82. });
  83. if (currentUser == null) {
  84. GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.skipped);
  85. }
  86. }
  87. catch (e) {
  88. logger.error(e);
  89. }
  90. }, [closeQuestionnaireModal, questionnaireOrder._id, currentUser]);
  91. const closeBtnClickHandlerClosingToast = useCallback(async() => {
  92. closeBtnClickHandler(true);
  93. }, [closeBtnClickHandler]);
  94. const questionnaireOrderTitle = lang === 'en_US' ? questionnaireOrder.title.en_US : questionnaireOrder.title.ja_JP;
  95. return (
  96. <Modal
  97. size="lg"
  98. isOpen={isOpened}
  99. toggle={closeBtnClickHandlerClosingToast}
  100. centered
  101. >
  102. <form onSubmit={submitHandler}>
  103. <ModalBody className="bg-primary overflow-hidden p-0" style={{ borderRadius: 8 }}>
  104. <div className="bg-white m-2 p-4" style={{ borderRadius: 8 }}>
  105. <div className="text-center mb-2">
  106. <h2 className="my-4">{questionnaireOrderTitle}</h2>
  107. <p className="mb-1">{t('commons:questionnaire_modal.more_satisfied_services')}</p>
  108. <p>{t('commons:questionnaire_modal.strive_to_improve_services')}</p>
  109. </div>
  110. <div className="container">
  111. <div className="row mt-4">
  112. <div className="col-md-2 offset-md-5 fw-bold text-end align-items-center p-0">{t('questionnaire.no_answer')}</div>
  113. <div className="col-md-5 d-flex justify-content-between align-items-center">
  114. <span className="fw-bold">{t('questionnaire.disagree')}</span>
  115. <span className="fw-bold">{t('questionnaire.agree')}</span>
  116. </div>
  117. </div>
  118. {questionnaireOrder.questions?.map((question) => {
  119. return <Question question={question} inputNamePrefix={inputNamePrefix} key={question._id} />;
  120. })}
  121. </div>
  122. <div className="text-center mt-5">
  123. <button type="submit" className="btn btn-primary">{t('questionnaire.answer')}</button>
  124. </div>
  125. <div className="text-center cursor-pointer text-decoration-underline my-3">
  126. <span style={{ cursor: 'pointer', textDecoration: 'underline' }} onClick={denyBtnClickHandler}>{t('questionnaire.dont_show_again')}</span>
  127. </div>
  128. {currentUser?.admin && (
  129. <a href="/admin/app#questionnaire-settings">
  130. <i className="material-symbols-outlined me-1">admin_panel_settings</i>
  131. </a>
  132. )}
  133. {currentUser != null && (
  134. <a href="/me#other_settings">
  135. <i className="material-symbols-outlined">settings</i>
  136. </a>
  137. )}
  138. </div>
  139. </ModalBody>
  140. </form>
  141. </Modal>
  142. );
  143. };
  144. export default QuestionnaireModal;