module Client.Features.Teacher.Questions.View open Feliz open Domain open Domain.Contracts open Client.Shared open Client.Features.Teacher.Questions.Types let private topicsView (model: Model) dispatch = Html.div [ prop.className "topics-panel" prop.children [ Html.h2 "Темы" match model.TopicsError with | Some err -> Html.p [ prop.className "error"; prop.text err ] | None -> Html.none if model.TopicsLoading then Html.p "Загрузка…" else Html.ul [ prop.children [ for topic in model.Topics -> Html.li [ prop.key (string topic.Id) prop.className ( if model.SelectedTopicId = Some topic.Id then "topic-item selected" else "topic-item" ) prop.onClick (fun _ -> dispatch (SelectTopic topic.Id)) prop.text topic.Name ] ] ] Html.form [ prop.onSubmit (fun e -> e.preventDefault () dispatch SubmitNewTopic) prop.children [ Html.input [ prop.type'.text prop.placeholder "Название новой темы" prop.value model.NewTopicForm.Name prop.onChange (SetNewTopicName >> dispatch) ] Html.button [ prop.type'.submit prop.disabled model.NewTopicForm.IsSubmitting prop.text "Добавить тему" ] match model.NewTopicForm.Error with | Some err -> Html.p [ prop.className "error"; prop.text err ] | None -> Html.none ] ] ] ] let private kindLabel (t: QuestionTypeView) = match t with | SingleChoiceT _ -> "Один вариант" | MultipleChoiceT _ -> "Несколько вариантов" | TrueFalseT _ -> "Верно/неверно" | ShortAnswerT _ -> "Короткий ответ" | NumericT _ -> "Числовой" /// Read-only rendering of a question's correct answer(s) for the teacher's /// question-bank table — distinct from `formFromSummary` in Types.fs, which /// unpacks the same data to prefill the edit form instead of displaying it. let private correctAnswerText (t: QuestionTypeView) = match t with | SingleChoiceT d -> d.Options |> List.tryFind (fun o -> o.Id = d.CorrectOptionId) |> Option.map (fun o -> o.Text) |> Option.defaultValue "—" | MultipleChoiceT d -> d.Options |> List.filter (fun o -> List.contains o.Id d.CorrectOptionIds) |> List.map (fun o -> o.Text) |> String.concat ", " | TrueFalseT b -> if b then "Верно" else "Неверно" | ShortAnswerT d -> String.concat ", " d.AcceptedAnswers | NumericT d -> sprintf "%s ± %s" (Format.points d.CorrectValue) (Format.points d.Tolerance) let private questionsListView (model: Model) dispatch = Html.div [ prop.className "questions-list" prop.children [ Html.h3 "Вопросы в теме" match model.QuestionsError with | Some err -> Html.p [ prop.className "error"; prop.text err ] | None -> Html.none if model.QuestionsLoading then Html.p "Загрузка…" elif model.Questions.IsEmpty then Html.p "В этой теме пока нет вопросов" else Html.table [ prop.className "questions-table" prop.children [ Html.thead [ Html.tr [ Html.th [ prop.text "Текст вопроса" ] Html.th [ prop.text "Тип" ] Html.th [ prop.text "Правильный ответ" ] Html.th [ prop.text "Баллы" ] Html.th [] ] ] Html.tbody [ for q in model.Questions -> Html.tr [ prop.key (string q.Id) prop.children [ Html.td [ prop.text q.Text ] Html.td [ prop.className "question-kind"; prop.text (kindLabel q.Type) ] Html.td [ prop.text (correctAnswerText q.Type) ] Html.td [ prop.className "tag-mono"; prop.text (Format.points q.Points) ] Html.td [ prop.className "question-actions-cell" prop.children [ Html.button [ prop.type'.button prop.onClick (fun _ -> dispatch (StartEditQuestion q.Id)) prop.text "Изменить" ] Html.button [ prop.type'.button prop.onClick (fun _ -> dispatch (RequestDeleteQuestion q.Id)) prop.text "Удалить" ] ] ] ] ] ] ] ] ] ] let private optionsEditor (form: NewQuestionForm) dispatch (multi: bool) = Html.div [ prop.className "options-editor" prop.children [ for opt in form.Options do Html.div [ prop.key (string opt.Id) prop.className "option-row" prop.children [ Html.input [ prop.type'.radio prop.name "correct-option" prop.isChecked (not multi && form.CorrectSingle = Some opt.Id) prop.style [ if multi then style.display.none ] prop.onChange (fun (_: bool) -> dispatch (SetCorrectSingle opt.Id)) ] Html.input [ prop.type'.checkbox prop.isChecked (form.CorrectMultiple.Contains opt.Id) prop.style [ if not multi then style.display.none ] prop.onChange (fun (_: bool) -> dispatch (ToggleCorrectMultiple opt.Id)) ] Html.input [ prop.type'.text prop.placeholder "Текст варианта" prop.value opt.Text prop.onChange (fun v -> dispatch (SetOptionText(opt.Id, v))) ] Html.button [ prop.type'.button prop.disabled (form.Options.Length <= 2) prop.onClick (fun _ -> dispatch (RemoveOption opt.Id)) prop.text "Удалить" ] ] ] Html.button [ prop.type'.button; prop.onClick (fun _ -> dispatch AddOption); prop.text "Добавить вариант" ] ] ] let private newQuestionFormView (isEditing: bool) (form: NewQuestionForm) dispatch = Html.form [ prop.className "new-question-form" prop.onSubmit (fun e -> e.preventDefault () dispatch SubmitNewQuestion) prop.children [ Html.h3 (if isEditing then "Редактирование вопроса" else "Новый вопрос") Html.label [ prop.text "Текст вопроса" ] Html.input [ prop.type'.text prop.value form.Text prop.onChange (SetQuestionText >> dispatch) ] Html.label [ prop.text "Баллы" ] Html.input [ prop.type'.number prop.value form.Points prop.onChange (SetQuestionPoints >> dispatch) ] Html.label [ prop.text "Тип вопроса" ] Html.select [ prop.value ( match form.Kind with | KSingleChoice -> "single" | KMultipleChoice -> "multiple" | KTrueFalse -> "truefalse" | KShortAnswer -> "short" | KNumeric -> "numeric" ) prop.onChange (fun (v: string) -> let kind = match v with | "single" -> KSingleChoice | "multiple" -> KMultipleChoice | "truefalse" -> KTrueFalse | "short" -> KShortAnswer | _ -> KNumeric dispatch (SetQuestionKind kind)) prop.children [ Html.option [ prop.value "single"; prop.text "Один вариант" ] Html.option [ prop.value "multiple"; prop.text "Несколько вариантов" ] Html.option [ prop.value "truefalse"; prop.text "Верно/неверно" ] Html.option [ prop.value "short"; prop.text "Короткий ответ" ] Html.option [ prop.value "numeric"; prop.text "Числовой" ] ] ] match form.Kind with | KSingleChoice -> optionsEditor form dispatch false | KMultipleChoice -> optionsEditor form dispatch true | KTrueFalse -> Html.div [ for value, text in [ true, "Верно"; false, "Неверно" ] -> Html.label [ prop.children [ Html.input [ prop.type'.radio prop.name "correct-bool" prop.isChecked (form.CorrectBool = value) prop.onChange (fun (_: bool) -> dispatch (SetCorrectBool value)) ] Html.text text ] ] ] | KShortAnswer -> Html.div [ Html.label [ prop.text "Допустимые ответы (через запятую)" ] Html.input [ prop.type'.text prop.value form.AcceptedAnswersText prop.onChange (SetAcceptedAnswersText >> dispatch) ] Html.label [ prop.children [ Html.input [ prop.type'.checkbox prop.isChecked form.CaseSensitive prop.onChange (SetCaseSensitive >> dispatch) ] Html.text "Учитывать регистр" ] ] ] | KNumeric -> Html.div [ Html.label [ prop.text "Правильное значение" ] Html.input [ prop.type'.text prop.value form.CorrectValueText prop.onChange (SetCorrectValueText >> dispatch) ] Html.label [ prop.text "Допустимая погрешность" ] Html.input [ prop.type'.text prop.value form.ToleranceText prop.onChange (SetToleranceText >> dispatch) ] ] match form.Error with | Some err -> Html.p [ prop.className "error"; prop.text err ] | None -> Html.none Html.button [ prop.type'.submit prop.disabled form.IsSubmitting prop.text ( if form.IsSubmitting then "Сохранение…" elif isEditing then "Сохранить изменения" else "Добавить вопрос" ) ] if isEditing then Html.button [ prop.type'.button prop.onClick (fun _ -> dispatch CancelEditQuestion) prop.text "Отмена" ] ] ] let view (model: Model) (dispatch: Msg -> unit) = Html.div [ prop.className "teacher-questions-page" prop.children [ Html.h1 "Банк вопросов" Html.div [ prop.className "teacher-layout" prop.children [ topicsView model dispatch match model.SelectedTopicId with | None -> Html.p "Выберите или создайте тему слева" | Some _ -> Html.div [ questionsListView model dispatch newQuestionFormView model.EditingQuestionId.IsSome model.NewQuestionForm dispatch ] ] ] ] ]