Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,6 @@ function App() {
path: "view-team-grades",
element: <ProtectedRoute element={<ReviewTable />} />,
},
{
path: "edit-questionnaire",
element: <ProtectedRoute element={<Questionnaire />} />,
},

{
path: "assignments/edit/:id",
element: <AssignmentEditor mode="update" />,
Expand Down Expand Up @@ -398,7 +393,6 @@ function App() {
},

{ path: "*", element: <NotFound /> },
{ path: "questionnaire", element: <Questionnaire />, loader: loadQuestionnaire },

{
path: "questionnaires",
Expand Down
199 changes: 199 additions & 0 deletions src/components/Modals/GradeExportModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// src/components/Modals/GradeExportModal.tsx
import React, { useState } from "react";
import { Modal, Button, Form, Row, Col } from "react-bootstrap";
import useAPI from "../../hooks/useAPI";
import { HttpMethod } from "../../utils/httpMethods";

/* ------------------------------------------------------------------ */
/* Shared text style */
/* ------------------------------------------------------------------ */
const TABLE_TEXT: React.CSSProperties = {
fontFamily: "verdana, arial, helvetica, sans-serif",
color: "#333",
fontSize: "15px",
lineHeight: "1.428em",
};

const STANDARD_TEXT: React.CSSProperties = {
fontFamily: "verdana, arial, helvetica, sans-serif",
color: "#333",
fontSize: "13px",
lineHeight: "30px",
};

/* ------------------------------------------------------------------ */
/* Export type options */
/* ------------------------------------------------------------------ */
type ExportType =
| "export_assignment_grades"
| "export_author_feedback_grades"
| "export_teammate_review_grades";

const EXPORT_OPTIONS: { value: ExportType; label: string; description: string }[] = [
{
value: "export_assignment_grades",
label: "Assignment Grades",
description: "One row per team with the instructor-assigned grade and comment.",
},
{
value: "export_author_feedback_grades",
label: "Author Feedback Grades",
description: "One row per participant with their average score from author feedback.",
},
{
value: "export_teammate_review_grades",
label: "Teammate Review Grades",
description: "One row per participant with their average teammate-review score.",
},
];

/* ------------------------------------------------------------------ */
/* Props */
/* ------------------------------------------------------------------ */
type GradeExportModalProps = {
show: boolean;
onHide: () => void;
assignmentId: number | string;
};

/* ================================================================== */
/* GradeExportModal Component */
/* ================================================================== */
const GradeExportModal: React.FC<GradeExportModalProps> = ({
show,
onHide,
assignmentId,
}) => {
const [selectedType, setSelectedType] = useState<ExportType>("export_assignment_grades");
const [status, setStatus] = useState<string>("");

const { isLoading, sendRequest: fetchExport } = useAPI();

/* Reset status when modal opens */
React.useEffect(() => {
if (show) setStatus("");
}, [show]);

/* Download helper — same pattern as ExportModal */
const downloadFile = (csvData: string, filename: string) => {
const url = window.URL.createObjectURL(new Blob([csvData], { type: "text/csv" }));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", filename);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
};

const onExport = async () => {
if (!assignmentId) {
setStatus("Assignment ID is missing.");
return;
}

setStatus("Generating CSV…");

try {
const response = await fetchExport({
url: `/grades/${assignmentId}/${selectedType}`,
method: HttpMethod.GET,
});

// fetchExport stores the result in useAPI's `data`; we need the raw response
// The hook returns void, so we rely on the data state — handled in useEffect below
} catch (err: any) {
setStatus(err.message || "Unexpected error.");
}
};

/* Listen to API response via a separate hook call */
const { data: exportResponse, error: exportError, sendRequest: doExport } = useAPI();

const handleExport = async () => {
if (!assignmentId) {
setStatus("Assignment ID is missing.");
return;
}
setStatus("Generating CSV…");
await doExport({
url: `/grades/${assignmentId}/${selectedType}`,
method: HttpMethod.GET,
});
};

React.useEffect(() => {
if (exportResponse) {
const { data: csvData, filename, message } = exportResponse.data;
setStatus(message || "Export complete.");
downloadFile(csvData, filename);
setTimeout(onHide, 1500);
} else if (exportError) {
setStatus(exportError);
}
}, [exportResponse, exportError]);

/* ---------------------------------------------------------------- */
/* Render */
/* ---------------------------------------------------------------- */
return (
<Modal
show={show}
onHide={onHide}
centered
size="lg"
keyboard
contentClassName="border border-2"
>
<Modal.Header closeButton style={{ ...STANDARD_TEXT, background: "#f7f8fa" }}>
<Modal.Title style={{ fontSize: 18, fontWeight: 600 }}>
Export Grades
</Modal.Title>
</Modal.Header>

<Modal.Body style={{ ...STANDARD_TEXT }}>
<Form.Label className="fw-semibold" style={TABLE_TEXT}>
Select grade type to export
</Form.Label>

{EXPORT_OPTIONS.map((opt) => (
<div key={opt.value} className="mb-2">
<Form.Check
type="radio"
name="grade_export_type"
id={`grade-export-${opt.value}`}
label={<strong style={TABLE_TEXT}>{opt.label}</strong>}
checked={selectedType === opt.value}
onChange={() => setSelectedType(opt.value)}
style={TABLE_TEXT}
/>
<div style={{ ...TABLE_TEXT, color: "#666", paddingLeft: 24, fontSize: 13 }}>
{opt.description}
</div>
</div>
))}

{status && (
<Row className="mt-3">
<Col>
<div style={{ ...TABLE_TEXT }}>
<strong>Status:</strong> {status}
</div>
</Col>
</Row>
)}
</Modal.Body>

<Modal.Footer style={{ ...STANDARD_TEXT }}>
<Button variant="outline-secondary" onClick={onHide}>
Cancel
</Button>
<Button variant="primary" onClick={handleExport} disabled={isLoading}>
Export
</Button>
</Modal.Footer>
</Modal>
);
};

export default GradeExportModal;
2 changes: 1 addition & 1 deletion src/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ const Header: React.FC = () => {
<NavDropdown.Item as={Link} to="/assignments">
Assignments
</NavDropdown.Item>
<NavDropdown.Item as={Link} to="/questionnaire">
<NavDropdown.Item as={Link} to="/questionnaires">
Questionnaire
</NavDropdown.Item>
<NavDropdown.Divider />
Expand Down
8 changes: 1 addition & 7 deletions src/pages/Questionnaires/Questionnaire.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const Questionnaires = () => {
const [showTypeModal, setShowTypeModal] = useState(false);

// loader option
const questionnaireData :any = useLoaderData();
const questionnaireData = useLoaderData() as QuestionnaireResponse[];

useEffect(() => {
setShowTypeModal(false);
Expand Down Expand Up @@ -63,12 +63,6 @@ const Questionnaires = () => {
[]
);


useEffect(() => {
if (error) {
dispatch(alertActions.showAlert({ variant: "danger", message: error }));
}
}, [error, dispatch]);

const tableColumns = useMemo(
() => questionnaireColumns(onEditHandle, onDeleteHandle),
Expand Down
20 changes: 12 additions & 8 deletions src/pages/Questionnaires/QuestionnaireDelete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,24 @@ interface IDeleteQuestionnaire {
const DeleteQuestionnaire: React.FC<IDeleteQuestionnaire> = ({ questionnaireData, onClose, onDeleteSuccess }) => {

const { data: deletedQuestionnaire, error: questionnaireError, sendRequest: deleteQuestionnaire } = useAPI();

const [show, setShow] = useState<boolean>(true);
const [isDeleting, setIsDeleting] = useState<boolean>(false);
const dispatch = useDispatch();


const deleteHandler = () =>
deleteQuestionnaire({
url: `/questionnaires/${questionnaireData.id}`,
method: HttpMethod.DELETE

const deleteHandler = () => {
setIsDeleting(true);
deleteQuestionnaire({
url: `/questionnaires/${questionnaireData.id}`,
method: HttpMethod.DELETE
});
};


useEffect(() => {
if (questionnaireError) {
setIsDeleting(false);
dispatch(alertActions.showAlert({ variant: "danger", message: questionnaireError }));
}
}, [questionnaireError, dispatch]);
Expand Down Expand Up @@ -73,8 +77,8 @@ const DeleteQuestionnaire: React.FC<IDeleteQuestionnaire> = ({ questionnaireData
<Button variant="outline-secondary" onClick={closeHandler}>
Cancel
</Button>
<Button variant="outline-danger" onClick={deleteHandler}>
Delete
<Button variant="outline-danger" onClick={deleteHandler} disabled={isDeleting}>
{isDeleting ? "Deleting..." : "Delete"}
</Button>
</Modal.Footer>
</Modal>
Expand Down
36 changes: 22 additions & 14 deletions src/pages/Questionnaires/QuestionnaireEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import axiosClient from "../../utils/axios_client";
import { IEditor } from "../../utils/interfaces";
import { QuestionnaireFormValues , transformQuestionnaireRequest } from "./QuestionnaireUtils";
import { IItem, QuestionnaireFormValues, transformQuestionnaireRequest } from "./QuestionnaireUtils";
import React, { useEffect, useState } from "react";
import { useLoaderData, useLocation, useNavigate, useSearchParams } from "react-router-dom";
import { Col, Container, Modal, Row } from 'react-bootstrap';
import { Col, Container, Row } from 'react-bootstrap';
import QuestionnaireForm from "./QuestionnaireForm";
import { useSelector} from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../store/store";
import { alertActions } from "../../store/slices/alertSlice";


const QuestionnaireEditor: React.FC<IEditor> = ({ mode }) => {
const token = localStorage.getItem("token");
const questionnaire :any = useLoaderData();
const questionnaire = useLoaderData() as QuestionnaireFormValues | undefined;
const [searchParams] = useSearchParams();
const location = useLocation();
const navigate = useNavigate();
const dispatch = useDispatch();
const type = searchParams.get("type");

const [fetchedItems, setFetchedItems] = useState<any[]>([]);
const [fetchedItems, setFetchedItems] = useState<IItem[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);


useEffect(() => {
Expand Down Expand Up @@ -48,27 +51,31 @@ const QuestionnaireEditor: React.FC<IEditor> = ({ mode }) => {
console.log("Type:", type);


// the form values to the browser console.
const onSubmit = async (values: QuestionnaireFormValues) => {
values.instructor_id = auth.user.id;
//values.instructor = auth.user.name;
console.log("Submit:", values);
const payload = transformQuestionnaireRequest(values);
const endpoint = mode === "create"
? "/questionnaires"
: `/questionnaires/${values.id}`;

setIsSubmitting(true);
try {
const response = await axiosClient[mode === "create" ? "post" : "put"](
await axiosClient[mode === "create" ? "post" : "put"](
endpoint,
payload,
payload,
{ headers: { Authorization: `Bearer ${token}` } }
);

console.log("Saved Questionnaire:", response.data);
dispatch(alertActions.showAlert({
variant: "success",
message: `Questionnaire "${values.name}" ${mode === "create" ? "created" : "updated"} successfully!`,
}));
navigate("/questionnaires");
} catch (error) {
console.error("Error submitting form:", error);
} catch (error: any) {
const message = error?.response?.data?.error ?? "Failed to save questionnaire. Please try again.";
dispatch(alertActions.showAlert({ variant: "danger", message }));
} finally {
setIsSubmitting(false);
}
};

Expand Down Expand Up @@ -106,7 +113,7 @@ const QuestionnaireEditor: React.FC<IEditor> = ({ mode }) => {
<Row className="mt-md-2 mb-md-2">
<Col className="text-center">
<h1>{mode === "update"
? `Update Questionnaire: ${questionnaire.name}`
? `Update Questionnaire: ${questionnaire?.name}`
: `Create ${type} Questionnaire`}</h1>
</Col>
<hr />
Expand All @@ -116,6 +123,7 @@ const QuestionnaireEditor: React.FC<IEditor> = ({ mode }) => {
<QuestionnaireForm
initialValues={initialValues}
onSubmit={onSubmit}
isSubmitting={isSubmitting}
/>
</Col>
</Row>
Expand Down
Loading