diff --git a/package-lock.json b/package-lock.json index 3ef4561e..ec5213eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@types/react-router-dom": "^5.3.3", "axios": "^1.4.0", "bootstrap": "^5.3.3", - "chart.js": "^3.7.0", + "chart.js": "^4.1.1", "formik": "^2.2.9", "jquery": "^3.7.1", "jwt-decode": "^3.1.2", @@ -38,7 +38,7 @@ "react-redux": "^8.0.5", "react-router-dom": "^6.11.1", "react-scripts": "^5.0.1", - "recharts": "^2.12.3", + "recharts": "^2.0.0", "redux-persist": "^6.0.0", "sass": "^1.62.1", "save": "^2.9.0", @@ -3045,6 +3045,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -5760,9 +5766,16 @@ } }, "node_modules/chart.js": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.0.tgz", - "integrity": "sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg==" + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz", + "integrity": "sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } }, "node_modules/check-types": { "version": "11.2.3", @@ -16603,6 +16616,28 @@ "node": ">= 0.8" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", diff --git a/src/App.tsx b/src/App.tsx index 27736ba3..9b46aff0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,7 +23,7 @@ import UserEditor from "./pages/Users/UserEditor"; import Users from "./pages/Users/User"; import { loadUserDataRolesAndInstitutions } from "./pages/Users/userUtil"; import Home from "pages/Home"; -import Questionnaire from "pages/EditQuestionnaire/Questionnaire"; +import Questionnaire from "pages/Questionnaires/Questionnaires"; import Courses from "pages/Courses/Course"; import CourseEditor from "pages/Courses/CourseEditor"; import { loadCourseInstructorDataAndInstitutions } from "pages/Courses/CourseUtil"; @@ -40,6 +40,14 @@ import ViewSubmissions from "pages/Assignments/ViewSubmissions"; import ViewScores from "pages/Assignments/ViewScores"; import ViewReports from "pages/Assignments/ViewReports"; import ViewDelayedJobs from "pages/Assignments/ViewDelayedJobs"; + + +// E2538 Additions +import QuestionnaireEditor from "pages/Questionnaires/QuestionnaireEditor"; +import { loadQuestionnaire } from "pages/Questionnaires/QuestionnaireUtils"; + + + function App() { const router = createBrowserRouter([ { @@ -285,11 +293,31 @@ function App() { { path: "questionnaire", element: , + loader: loadQuestionnaire, }, ], }, { path: "*", element: }, - { path: "questionnaire", element: }, // Added the Questionnaire route + + + // FIxME: Project 4 E2538 + { + path: "questionnaires", + element: } leastPrivilegeRole={ROLE.INSTRUCTOR} />, + loader: loadQuestionnaire, + children: [ + { + path: "new", + element: , + loader: loadQuestionnaire, + }, + { + path: "edit/:id", + element: , + loader: loadQuestionnaire, + }, + ], + }, ], }, ]); diff --git a/src/components/Alert.tsx b/src/components/Alert.tsx index 820b73ca..faf29a50 100644 --- a/src/components/Alert.tsx +++ b/src/components/Alert.tsx @@ -7,7 +7,7 @@ import { alertActions } from "store/slices/alertSlice"; * @author Ankur Mundra on May, 2023 */ -interface IAlertProps { +export interface IAlertProps { variant: string; title?: string; message: string; diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 8be9ca9c..ef7607ac 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -146,7 +146,6 @@ const Table: React.FC = ({ return ( <> - {isGlobalFilterVisible && ( @@ -158,11 +157,9 @@ const Table: React.FC = ({ {isGlobalFilterVisible ? " Hide" : " Show"} {" "} - - - + {getHeaderGroups().map((headerGroup) => ( @@ -226,7 +223,6 @@ const Table: React.FC = ({ )} - ); }; diff --git a/src/layout/Header.tsx b/src/layout/Header.tsx index 5b278dd8..949750db 100644 --- a/src/layout/Header.tsx +++ b/src/layout/Header.tsx @@ -128,7 +128,7 @@ const Header: React.FC = () => { Assignments - + Questionnaire diff --git a/src/pages/Questionnaire/questionnaire.tsx b/src/pages/Questionnaire/questionnaire.tsx deleted file mode 100644 index 9f3361b5..00000000 --- a/src/pages/Questionnaire/questionnaire.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import React, { useState } from 'react'; -import './Questionnaire.css'; -import {Button} from "react-bootstrap"; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTrash, faPencilAlt, faEye } from '@fortawesome/free-solid-svg-icons'; -import dummyData from './dummyData.json'; -import {BsPencilFill, BsPersonXFill} from "react-icons/bs"; -import {BiCopy}from "react-icons/bi"; -import { BsPlusSquareFill } from "react-icons/bs"; - - -function Questionnaire() { - const [showOnlyMyItems, setShowOnlyMyItems] = useState(true); - const [expandedItem, setExpandedItem] = useState(null); - const [sortOrder, setSortOrder] = useState<'asc' | 'desc' | 'default' | null>(null); - - const questionnaireItems = dummyData; // Use dummy data for items - - const handleAddButtonClick = () => { - console.log('Add button clicked'); - // Add your logic for adding questionnaire items here - }; - type QuestionnaireItem = { - name: string; - creationDate: string; - updatedDate: string; - }; - - - const handleItemClick = (index: number) => { - if (expandedItem === index) { - setExpandedItem(null); - } else { - setExpandedItem(index); - } - }; - - const handleDelete = (item: QuestionnaireItem) => { - console.log(`Delete button clicked for item:`, item); - // Add your logic for deleting the item here - }; - - const handleEdit = (item: QuestionnaireItem) => { - console.log(`Edit button clicked for item:`, item); - // Add your logic for editing the item here - }; - - const handleShow = (item: QuestionnaireItem) => { - console.log(`Show button clicked for item:`, item); - // Add your logic for showing the item here - }; - - const handleSortByName = () => { - if (sortOrder === 'asc') { - setSortOrder('desc'); - } else { - setSortOrder('asc'); - } - }; - - const sortedQuestionnaireItems = [...questionnaireItems]; - - if (sortOrder === 'asc') { - sortedQuestionnaireItems.sort(); - } else if (sortOrder === 'desc') { - sortedQuestionnaireItems.sort().reverse(); - } - - return ( -
-

Questionnaire List

- - -
- - - - - - - - - - - - {sortedQuestionnaireItems.map((item, index) => ( - - - - - - {expandedItem === index && ( - - - -)} - - ))} - -
- Name {sortOrder === 'asc' && '↑'} {sortOrder === 'desc' && '↓'} {sortOrder === null && '↑↓'} - Action
handleItemClick(index)}>{item.name} - - - -
- - - - - - - - - - - - - - - - - - - - - - - - -
Name:Creation Date:Updated Date:Actions:
{item.name}{item.creationDate}{item.updatedDate} - - - - - - - - - -
-
-
- ); -} - -export default Questionnaire; \ No newline at end of file diff --git a/src/pages/Questionnaire/Questionnaire.css b/src/pages/Questionnaires/Questionnaire.css similarity index 79% rename from src/pages/Questionnaire/Questionnaire.css rename to src/pages/Questionnaires/Questionnaire.css index 5362989a..19f7f4cd 100644 --- a/src/pages/Questionnaire/Questionnaire.css +++ b/src/pages/Questionnaires/Questionnaire.css @@ -4,12 +4,13 @@ align-items: center; text-align: center; min-height: 100vh; - padding-top: 20px; /* Add padding to the top */ + padding-top: 20px; } + .questionnaire-table { border-collapse: collapse; - width: 70%; /* Adjust the width as needed */ - margin-top: 20px; /* Add some spacing from the button and label */ + width: 70%; + margin-top: 20px; } .questionnaire-table th, .questionnaire-table td { @@ -29,11 +30,11 @@ .questionnaire-table tr:hover { background-color: #ddd; } + .centered-table { display: flex; justify-content: center; align-items: center; text-align: center; height: 100%; - } - \ No newline at end of file + } \ No newline at end of file diff --git a/src/pages/Questionnaires/QuestionnaireColumns.tsx b/src/pages/Questionnaires/QuestionnaireColumns.tsx new file mode 100644 index 00000000..0a4604e2 --- /dev/null +++ b/src/pages/Questionnaires/QuestionnaireColumns.tsx @@ -0,0 +1,63 @@ +import { BsPencilFill, BsPersonXFill } from "react-icons/bs"; +import { Row, createColumnHelper } from "@tanstack/react-table"; + +import { Button } from "react-bootstrap"; +import { QuestionnaireResponse as IQuestionnaire } from "./QuestionnaireUtils"; + +type Fn = (row: Row) => void; +const columnHelper = createColumnHelper(); + + +export const questionnaireColumns = (handleEdit: Fn, handleDelete: Fn) => [ + columnHelper.accessor("id", { + header: "ID", + }), + columnHelper.accessor("name", { + header: "Name", + }), + columnHelper.accessor("private", { + header: "Private", + }), + columnHelper.accessor("min_question_score", { + header: "Min. Question Score", + }), + columnHelper.accessor("max_question_score", { + header: "Max. Question Score", + }), + columnHelper.accessor("questionnaire_type", { + header: "Type", + }), + columnHelper.accessor("instructor_id", { + header: "Instructor ID", + }), + columnHelper.accessor("instructor.name", { + header: "Instructor Name", + }), + columnHelper.accessor("instructor.email", { + header: "Instructor Email", + }), + columnHelper.display({ + id: "actions", + header: "Actions", + cell: ({ row }) => ( + <> + + + + + ), + }), +]; \ No newline at end of file diff --git a/src/pages/Questionnaires/QuestionnaireEditor.tsx b/src/pages/Questionnaires/QuestionnaireEditor.tsx new file mode 100644 index 00000000..95bdc29e --- /dev/null +++ b/src/pages/Questionnaires/QuestionnaireEditor.tsx @@ -0,0 +1,106 @@ +import axiosClient from "../../utils/axios_client"; +import { IEditor } from "../../utils/interfaces"; +import { QuestionnaireFormValues , transformQuestionnaireRequest } from "./QuestionnaireUtils"; +import React from "react"; +import { useLoaderData, useLocation, useNavigate, useSearchParams } from "react-router-dom"; +import { Modal } from 'react-bootstrap'; +import QuestionnaireForm from "./QuestionnaireForm"; + + +const QuestionnaireEditor: React.FC = ({ mode }) => { + const token = localStorage.getItem("token"); + const questionnaire :any = useLoaderData(); + const [searchParams] = useSearchParams(); + const location = useLocation(); + const navigate = useNavigate(); + const type = searchParams.get("type"); + + // Can view the decoded type in browser console + console.log("Type:", type); + + + // FIXME: See note below + // const onSubmit = async (values: QuestionnaireFormValues) => { + // console.log("Submit:", values); + // console.log("Submit:", values.items); + // }; + + + // FIXME: This only works with slight changes to the backend. + // Added to the Questionnaire model "accepts_nested_attributes_for :items, allow_destroy: true" + // allows the items belonging to a questionnaire to be saved at the same time. This also + // requires updating the questionnaire_params in questionnaire_controller.rb to include item_attributes. + // Including this method without theses changes will result in a failure to submit the form. + // Comment out this version of onSubmit, and include the implementation above to simply print + // the form values to the browser console. + const onSubmit = async (values: any) => { + console.log("hello"); + console.log(values.items); + // transform the values before submitting + const transformedValues = transformQuestionnaireRequest(values); + + // determine the endpoint based on whether we're creating or updating + const endpoint = mode === "create" + ? "/questionnaires" // creating questionnaires + : `/questionnaires/${values.id}`; // updating questionnaires + + try { + // submit the transformed values to the API + const response = await axiosClient[mode === "create" ? "post" : "put"](endpoint, transformedValues, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + console.log("Server Response:", response.data); + + // navigate back to the questionnaire management dashboard + navigate(`/questionnaires`); + } catch (error) { + console.error("Error submitting form:", error); + } + }; + + + // initial form values + const initialValues: QuestionnaireFormValues = { + id: questionnaire?.id ?? undefined, + name: questionnaire?.name ?? "", + questionnaire_type: questionnaire?.questionnaire_type ?? type ?? "", + private: questionnaire?.private ?? false, + min_question_score: questionnaire?.min_question_score ?? 0, + max_question_score: questionnaire?.max_question_score ?? 10, + items: questionnaire?.items ?? [ { + txt: "", + weight: 1, + question_type: "", + break_before: 1, + alternatives: "", + min_label: 0, + max_label: 10, + seq: 1, + }, + ], + }; + + const handleClose = () => navigate(location.state?.from ? location.state.from : "/questionnaires"); + + return ( + + + + {mode === "update" + ? `Update Questionnaire - ${questionnaire.name}` + : `Create ${type} Questionnaire`} + + + + + + + ); +}; + +export default QuestionnaireEditor; \ No newline at end of file diff --git a/src/pages/Questionnaires/QuestionnaireForm.tsx b/src/pages/Questionnaires/QuestionnaireForm.tsx new file mode 100644 index 00000000..dbc56536 --- /dev/null +++ b/src/pages/Questionnaires/QuestionnaireForm.tsx @@ -0,0 +1,141 @@ +import React from "react"; +import { Formik, Field, Form, ErrorMessage } from "formik"; +import { Button } from 'react-bootstrap'; +import QuestionnaireItemsFieldArray from "./QuestionnaireItemsFieldArray"; +import * as Yup from "yup"; + + +const QuestionnaireForm = ({ initialValues, onSubmit }: any) => { + + const itemFields = Yup.object().shape({ + txt: Yup.string().required("Question text is required"), + question_type: Yup.string().required("Question type is required"), + weight: Yup.number() + .required("Weight is required") + .positive("Weight must be a positive number"), + + alternatives: Yup.string().when("question_type", ([questionType], schema) => { + if (questionType === "dropdown" || questionType === "multiple_choice") { + return schema + .required("Options are required") + .test( + "min-2-options", + "Enter at least two options, separated by commas.", + (value) => { + if (!value) return false; + const options = value + .split(",") + .map((opt) => opt.trim()) + .filter((opt) => opt !== ""); + return options.length >= 2; + } + ); + } + return schema.notRequired(); + }), + + min_label: Yup.string().when("question_type", ([question_type], schema) => { + return question_type === "scale" + ? schema.required("Minimum label is required") + : schema.notRequired(); + }), + + max_label: Yup.string().when("question_type", ([question_type], schema) => { + return question_type === "scale" + ? schema.required("Maximum label is required") + : schema.notRequired(); + }), + }); + + const validationSchema = Yup.object().shape({ + name: Yup.string().required("Name is required"), + questionnaire_type: Yup.string().required("Questionnaire type is required"), + private: Yup.boolean(), + min_question_score: Yup.number().required("Minimum question score is required"), + max_question_score: Yup.number().required("Maximum question score is required"), + items: Yup.array().of(itemFields).min(1, "At least one question is required"), + }); + + + return ( + + {({ values, handleChange, errors, touched }) => ( +
+
Name
+ + + + + + + +
+ +
Private
+ + + +
+ +
Minimum Question Score
+ + + +
+ +
Maximum Question Score
+ + + +
+ + {/* Allows users to input a variable number of questions / items */} +
Items
+ + +
+ + + )} +
+ ); +}; + +export default QuestionnaireForm; \ No newline at end of file diff --git a/src/pages/Questionnaires/QuestionnaireItemsFieldArray.tsx b/src/pages/Questionnaires/QuestionnaireItemsFieldArray.tsx new file mode 100644 index 00000000..b5df4ee0 --- /dev/null +++ b/src/pages/Questionnaires/QuestionnaireItemsFieldArray.tsx @@ -0,0 +1,155 @@ +import React from "react"; +import { Field, FieldArray, FieldArrayRenderProps, ErrorMessage } from "formik"; +import { IoIosRemoveCircleOutline, IoIosAddCircleOutline } from "react-icons/io"; + +interface Props { + values: any; + errors: any; + touched: any; +} + +const QuestionnaireItemsFieldArray: React.FC = ({ values, errors, touched }) => { + return ( + + {({ push, remove }: FieldArrayRenderProps) => ( + <> + {values.items.map((item: any, index: number) => ( +
+
+ Item {index + 1} + { values.items[index]._destroy = true; remove(index)}} + title="Remove question" + /> +
+ + + + + + + + + + + + + + + + {(item.question_type === "dropdown" || item.question_type === "multiple_choice") && ( + <> + + + )} + + {item.question_type === "scale" && ( +
+ + +
+ )} + + + + + {/*error messages for each questionnaire item rendered below all fields*/} + + + + + + + {(item.question_type === "dropdown" || item.question_type === "multiple_choice") && ( + <> + + + )} + + {(item.question_type === "scale") && ( + <> + + + + )} +
+ ))} + +
+ + push({ + txt: "", + weight: 1, + question_type: "", + break_before: 1, + alternatives: "", + min_label: 0, + max_label: 10, + seq: values.items.length + 1, + }) + } + title="Add question" + /> + Add Question +
+ + )} +
+ ); +}; + +export default QuestionnaireItemsFieldArray; diff --git a/src/pages/Questionnaires/QuestionnaireTypes.tsx b/src/pages/Questionnaires/QuestionnaireTypes.tsx new file mode 100644 index 00000000..f899c30d --- /dev/null +++ b/src/pages/Questionnaires/QuestionnaireTypes.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import Table from "components/Table/Table"; +import { QuestionnaireTypes, QuestionnaireType } from "./QuestionnaireUtils"; +import { ColumnDef } from "@tanstack/react-table"; +import { useNavigate } from "react-router-dom"; +import { IoIosAddCircle } from "react-icons/io"; + + +interface TableRow { + type: QuestionnaireType; +} + +const QuestionnaireTypeTable: React.FC = () => { + const data: TableRow[] = QuestionnaireTypes.map((type) => ({ type })); + const navigate = useNavigate(); + + const onCreate = (type: QuestionnaireType) => { + // Navigate to the Questionnaire's new form + navigate(`/questionnaires/new?type=${encodeURIComponent(type)}`); + }; + + const columns: ColumnDef[] = [ + { + header: "Questionnaire Type", + accessorKey: "type", + cell: (info) => info.getValue(), + size: 500, + minSize: 80, + maxSize: 600, + }, + { + header: "Action", + id: "action", + cell: ({ row }) => { + const type = row.original.type; + return ( + onCreate(type)} + style={{ + cursor: "pointer", + transition: "all 0.2s", + color: "#3b82f6", + }} + onMouseEnter={(e) => { + e.currentTarget.style.color = "#2563eb"; + }} + onMouseLeave={(e) => { + e.currentTarget.style.color = "#3b82f6"; + }} + size={24} + /> + ); + }, + }, + ]; + + return ( + = 10} + /> + ); +}; + +export default QuestionnaireTypeTable; \ No newline at end of file diff --git a/src/pages/Questionnaires/QuestionnaireUtils.ts b/src/pages/Questionnaires/QuestionnaireUtils.ts new file mode 100644 index 00000000..b12ac22e --- /dev/null +++ b/src/pages/Questionnaires/QuestionnaireUtils.ts @@ -0,0 +1,143 @@ +import axiosClient from "../../utils/axios_client"; +import { IInstructor } from "../../utils/interfaces"; + +export type QuestionnaireType = + | "Metareview" + | "Author Feedback" + | "Teammate Review" + | "Survey" + | "Assignment Survey" + | "Global Survey" + | "Course Survey" + | "Bookmark Rating" + | "Quiz"; + + +export const QuestionnaireTypes: QuestionnaireType[] = [ + "Metareview", + "Author Feedback", + "Teammate Review", + "Survey", + "Assignment Survey", + "Global Survey", + "Course Survey", + "Bookmark Rating", + "Quiz", +]; + + +export interface IItem { + id?: number; + txt: string; + weight: number; + seq: number; + question_type: string; + size: number; + alternatives: string; + min_label: number; + max_label: number; + break_before: number; + questionnaire_id?: number; + _destroy?: boolean; +} + + +export interface QuestionnaireFormValues { + id?: number; + name: string; + questionnaire_type:string; + private:boolean; + min_question_score: number; + max_question_score: number; + instructor_id?: number; + instructor?: IInstructor; + items?: IItem[]; +} + +export interface QuestionnaireResponse { + id?: number; + name: string; + private:boolean; + questionnaire_type:string; + min_question_score: number; + max_question_score: number; + instructor_id: number; + instructor: IInstructor; + items?: IItem[]; +} + +export interface QuestionnaireRequest { + id?: number; + name: string; + private:boolean; + questionnaire_type:string; + min_question_score: number; + max_question_score: number; + instructor_id?: number; + instructor?: IInstructor; + items_attributes: IItem[]; +} + +export function getQuestionnaireTypes(quest: QuestionnaireResponse[]): string[] { + return Array.from( + new Set( + quest + .map((q) => q.questionnaire_type) + .filter((type): type is string => type !== null) + ) + ); +} + + +export const transformQuestionnaireRequest = (values: QuestionnaireFormValues) => { + const questionnaire: QuestionnaireRequest = { + name: values.name, + questionnaire_type:values.questionnaire_type, + private:values.private, + min_question_score: values.min_question_score, + max_question_score:values.max_question_score, + instructor_id:values.instructor_id, + instructor:values.instructor, + items_attributes:values.items ? values.items : [], + }; + console.log("Transformed Questionnaire Request:", questionnaire); + return JSON.stringify(questionnaire); +}; + +export const transformQuestionnaireResponse = (questionnaireResponse: string) => { + const questionnaire: QuestionnaireResponse = JSON.parse(questionnaireResponse); + const questionnaireValues: QuestionnaireFormValues = { + id: questionnaire.id, + name: questionnaire.name, + private:questionnaire.private, + questionnaire_type:questionnaire.questionnaire_type, + min_question_score: questionnaire.min_question_score, + max_question_score:questionnaire.max_question_score, + instructor_id:questionnaire.instructor_id, + instructor:questionnaire.instructor, + items:questionnaire.items, + }; + return questionnaireValues; +}; + +export async function loadQuestionnaire({ params }: any) { + let questionnaireData = {}; + // if params contains id, then we are editing a user, so we need to load the user data + if (params.id) { + const questionnaireResponse = await axiosClient.get(`/questionnaires/${params.id}`, { + transformResponse: transformQuestionnaireResponse, + }); + questionnaireData = await questionnaireResponse.data; + } + else { + // Fetch all questionnaires + const questionnaireResponse = await axiosClient.get(`/questionnaires`); + questionnaireData = questionnaireResponse.data.map((q: any) => + transformQuestionnaireResponse(JSON.stringify(q)) + ); + } + + console.log(questionnaireData); + return questionnaireData; +} + diff --git a/src/pages/Questionnaires/Questionnaires.tsx b/src/pages/Questionnaires/Questionnaires.tsx new file mode 100644 index 00000000..371ef080 --- /dev/null +++ b/src/pages/Questionnaires/Questionnaires.tsx @@ -0,0 +1,105 @@ +import { Button, Col, Container, Modal, Row } from "react-bootstrap"; +import { Outlet, useLoaderData, useLocation, useNavigate } from "react-router-dom"; +import React, { useCallback, useMemo, useState, useEffect } from "react"; +import { questionnaireColumns } from "./QuestionnaireColumns"; +import { BsFileText } from "react-icons/bs"; +import { QuestionnaireResponse } from "./QuestionnaireUtils"; +import { Row as TRow } from "@tanstack/react-table"; +import Table from "components/Table/Table"; +import QuestionnaireTypeTable from "./QuestionnaireTypes"; + + +// This is just a table of questionnaires and their corresponding details. +// The reimplemenentation back end doesn't appear to have a predefined list of questionnaire types. +// getQuestionnaireTypes in QuestionnaireUtils.tsx can be used to extract unique types from the +// list of all questionnaires. Alternatively, a constant list of types is included in the current +// Expertiza questionnaire model that can be used as a guide to define a constant list allowed types. +// Because using getQuestionnaireTypes will only extract the type from existing questionnaires, +//. types with no instantiations would be missing using this method. + + +const Questionnaires = () => { + const navigate = useNavigate(); + const location = useLocation(); + const [showTypeModal, setShowTypeModal] = useState(false); + + // loader option + const quest :any = useLoaderData(); + console.log(quest); + + useEffect(() => { + setShowTypeModal(false); + }, [location]); + + const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<{ + visible: boolean; + data?: QuestionnaireResponse; + }>({ visible: false }); + + const onDeleteQuestionnaireHandler = useCallback(() => setShowDeleteConfirmation({ visible: false }), []); + + const onEditHandle = useCallback( + (row: TRow) => navigate(`edit/${row.original.id}`), + [navigate] + ); + + const onDeleteHandle = useCallback( + (row: TRow) => setShowDeleteConfirmation({ visible: true, data: row.original }), + [] + ); + + const tableColumns = useMemo( + () => questionnaireColumns(onEditHandle, onDeleteHandle), + [onDeleteHandle, onEditHandle] + ); + + const handleClose = () => setShowTypeModal(false); + + return ( + <> + +
+ + +
+

Manage Questionnaires

+ +
+ + + + + + + + {showTypeModal && ( + + + Select Questionnaire Type + + + + + + )} + + +
+ + + + + ); +}; + +export default Questionnaires; \ No newline at end of file diff --git a/src/pages/Questionnaire/dummyData.json b/src/pages/Questionnaires/dummyData.json similarity index 100% rename from src/pages/Questionnaire/dummyData.json rename to src/pages/Questionnaires/dummyData.json diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index 213909c9..99e70c19 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -20,6 +20,7 @@ export interface IInstitution { export interface IInstructor { id?: number; name: string; + email?: string; } export interface ITA {