diff --git a/package-lock.json b/package-lock.json index 3ef4561e..768993a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,10 +24,11 @@ "@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", + "mysql2": "^3.14.0", "react": "^18.2.0", "react-bootstrap": "^2.7.4", "react-chartjs-2": "^5.2.0", @@ -38,7 +39,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 +3046,11 @@ "@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==" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -5178,6 +5184,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/axe-core": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", @@ -5760,9 +5775,15 @@ } }, "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==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } }, "node_modules/check-types": { "version": "11.2.3", @@ -6879,6 +6900,15 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -8688,6 +8718,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -9748,6 +9787,12 @@ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -11270,6 +11315,12 @@ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" }, + "node_modules/long": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", + "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -11297,6 +11348,21 @@ "yallist": "^3.0.2" } }, + "node_modules/lru.min": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -11598,6 +11664,26 @@ "multicast-dns": "cli.js" } }, + "node_modules/mysql2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.0.tgz", + "integrity": "sha512-8eMhmG6gt/hRkU1G+8KlGOdQi2w+CgtNoD1ksXZq9gQfkfDsX4LHaBwTe1SY0Imx//t2iZA03DFnyYKPinxSRw==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -11608,6 +11694,27 @@ "thenify-all": "^1.0.0" } }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -14925,6 +15032,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -15232,6 +15344,15 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", @@ -16603,6 +16724,27 @@ "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==", + "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/package.json b/package.json index a1ed9d64..2d7668c8 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,10 @@ "axios": "^1.4.0", "bootstrap": "^5.3.3", "chart.js": "^4.1.1", - "recharts": "^2.0.0", "formik": "^2.2.9", "jquery": "^3.7.1", "jwt-decode": "^3.1.2", + "mysql2": "^3.14.0", "react": "^18.2.0", "react-bootstrap": "^2.7.4", "react-chartjs-2": "^5.2.0", @@ -34,6 +34,7 @@ "react-redux": "^8.0.5", "react-router-dom": "^6.11.1", "react-scripts": "^5.0.1", + "recharts": "^2.0.0", "redux-persist": "^6.0.0", "sass": "^1.62.1", "save": "^2.9.0", diff --git a/public/assets/icons/copy-temp.png b/public/assets/icons/copy-temp.png new file mode 100644 index 00000000..1a88fff3 Binary files /dev/null and b/public/assets/icons/copy-temp.png differ diff --git a/public/assets/icons/delete-temp.png b/public/assets/icons/delete-temp.png new file mode 100644 index 00000000..dff8fb65 Binary files /dev/null and b/public/assets/icons/delete-temp.png differ diff --git a/public/assets/icons/edit-temp.png b/public/assets/icons/edit-temp.png new file mode 100644 index 00000000..4ec0860c Binary files /dev/null and b/public/assets/icons/edit-temp.png differ diff --git a/public/assets/icons/export-temp.png b/public/assets/icons/export-temp.png new file mode 100644 index 00000000..8f8a6146 Binary files /dev/null and b/public/assets/icons/export-temp.png differ diff --git a/public/assets/icons/info.png b/public/assets/icons/info.png new file mode 100644 index 00000000..e0626bec Binary files /dev/null and b/public/assets/icons/info.png differ diff --git a/src/App.tsx b/src/App.tsx index 27736ba3..9db7008d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -27,6 +27,8 @@ import Questionnaire from "pages/EditQuestionnaire/Questionnaire"; import Courses from "pages/Courses/Course"; import CourseEditor from "pages/Courses/CourseEditor"; import { loadCourseInstructorDataAndInstitutions } from "pages/Courses/CourseUtil"; +import StudentTasks from "./pages/StudentTasks/StudentTasks"; +import StudentTaskDetail from "pages/StudentTasks/StudentTaskDetail"; import TA from "pages/TA/TA"; import TAEditor from "pages/TA/TAEditor"; import { loadTAs } from "pages/TA/TAUtil"; @@ -59,6 +61,22 @@ function App() { path: "edit-questionnaire", element: } />, }, + { + path: "student_tasks", + element: } leastPrivilegeRole={ROLE.STUDENT} />, + // children: [ + // { + // path: ":id", + // element: , + // }, + // ], + }, + { + path: "student_task_detail/:id", + element: ( + } leastPrivilegeRole={ROLE.STUDENT} /> + ), + }, { path: "assignments/edit/:id/createteams", element: , @@ -192,11 +210,11 @@ function App() { }, { path: "reviews", - element: , + element: , }, { path: "email_the_author", - element: , + element: , }, // Fixed the missing comma and added an opening curly brace { diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 8be9ca9c..277cd604 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -8,18 +8,18 @@ import { getSortedRowModel, SortingState, useReactTable, + ExpandedState, + getExpandedRowModel, } from "@tanstack/react-table"; import React, { useEffect, useMemo, useRef, useState } from "react"; import { Col, Container, Row, Table as BTable } from "react-bootstrap"; +import { BsChevronRight, BsChevronDown } from "react-icons/bs"; import ColumnFilter from "./ColumnFilter"; import GlobalFilter from "./GlobalFilter"; import Pagination from "./Pagination"; import RowSelectCheckBox from "./RowSelectCheckBox"; import { FaSearch } from "react-icons/fa"; - -/** - * @author Ankur Mundra on May, 2023 - */ +import ToolTip from "components/ToolTip"; interface TableProps { data: Record[]; @@ -30,6 +30,10 @@ interface TableProps { tableSize?: { span: number; offset: number }; columnVisibility?: Record; onSelectionChange?: (selectedData: Record[]) => void; + renderSubComponent?: (props: { row: any }) => React.ReactNode; + getRowCanExpand?: (row: any) => boolean; + disableGlobalFilter?: boolean; // Disable the Global Search + headingComments?: Record; } const Table: React.FC = ({ @@ -41,89 +45,105 @@ const Table: React.FC = ({ onSelectionChange, columnVisibility = {}, tableSize = { span: 12, offset: 0 }, + renderSubComponent, + getRowCanExpand, + disableGlobalFilter = false, // Disable the Global Search + headingComments = {}, }) => { - const colsPlusSelectable = useMemo(() => { - const selectableColumn: any = { - id: "select", - header: ({ table }: any) => { - return ( - - ); - }, - cell: ({ row }: any) => { - return ( - - ); - }, - enableSorting: false, - enableFilter: false, - }; - return [selectableColumn, ...columns]; - }, [columns]); - const [rowSelection, setRowSelection] = useState({}); const [sorting, setSorting] = useState([]); const [globalFilter, setGlobalFilter] = useState(""); const [columnFilters, setColumnFilters] = useState([]); const [columnVisibilityState, setColumnVisibilityState] = useState(columnVisibility); - const [isGlobalFilterVisible, setIsGlobalFilterVisible] = useState(showGlobalFilter); // State for global filter visibility + const [isGlobalFilterVisible, setIsGlobalFilterVisible] = useState(showGlobalFilter); + const [expanded, setExpanded] = useState({}); const selectable = typeof onSelectionChange === "function"; const onSelectionChangeRef = useRef(onSelectionChange); + const colsPlusExpander = useMemo(() => { + if (!renderSubComponent) return columns; + + const expanderColumn: ColumnDef = { + id: "expander", + header: () => null, + cell: ({ row }) => { + if (getRowCanExpand ? !getRowCanExpand(row) : false) { + return null; + } + return ( + + ); + }, + enableSorting: false, + enableColumnFilter: false, + }; + + const selectableColumn = selectable + ? [ + { + id: "select", + header: ({ table }: any) => ( + + ), + cell: ({ row }: any) => ( + + ), + enableSorting: false, + enableFilter: false, + }, + ] + : []; + + return [...selectableColumn, expanderColumn, ...columns]; + }, [columns, selectable, renderSubComponent, getRowCanExpand]); + const table = useReactTable({ data: initialData, - columns: selectable ? colsPlusSelectable : columns, + columns: colsPlusExpander, state: { sorting, globalFilter, columnFilters, rowSelection, columnVisibility: columnVisibilityState, + expanded, }, onSortingChange: setSorting, onRowSelectionChange: setRowSelection, onGlobalFilterChange: setGlobalFilter, onColumnFiltersChange: setColumnFilters, onColumnVisibilityChange: setColumnVisibilityState, + onExpandedChange: setExpanded, + getRowCanExpand, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: getPaginationRowModel(), + getExpandedRowModel: getExpandedRowModel(), }); - const { - getState, - getHeaderGroups, - getRowModel, - getCanNextPage, - getCanPreviousPage, - previousPage, - nextPage, - setPageIndex, - setPageSize, - getPageCount, - } = table; - - // Used to return early from useEffect() on mount. - const firstRenderRef = useRef(true); - // This useEffect() watches flatRows such that on change it - // calls the onSelectionChange() prop. Technically, it calls - // the onSelectionChangeRef.current function if it exists. - const flatRows = table.getSelectedRowModel().flatRows; useEffect(() => { @@ -144,6 +164,8 @@ const Table: React.FC = ({ setIsGlobalFilterVisible(!isGlobalFilterVisible); }; + const firstRenderRef = useRef(true); + return ( <> @@ -153,10 +175,12 @@ const Table: React.FC = ({ )} - - - {isGlobalFilterVisible ? " Hide" : " Show"} - {" "} + {!disableGlobalFilter && ( + + + {isGlobalFilterVisible ? " Hide" : " Show"} + + )} @@ -164,9 +188,11 @@ const Table: React.FC = ({ - {getHeaderGroups().map((headerGroup) => ( + {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { + // Add info icon to Heading if comment exists. + const comment = headingComments[header.column.columnDef.header as string]; return ( {header.isPlaceholder ? null : ( @@ -180,6 +206,7 @@ const Table: React.FC = ({ }} > {flexRender(header.column.columnDef.header, header.getContext())} + {comment && } {{ asc: " 🔼", desc: " 🔽", @@ -197,31 +224,36 @@ const Table: React.FC = ({ ))} - {getRowModel().rows.map((row) => { - return ( - - {row.getVisibleCells().map((cell) => { - return ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ); - })} + {table.getRowModel().rows.map((row) => ( + + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} - ); - })} + {row.getIsExpanded() && renderSubComponent && ( + + + {renderSubComponent({ row })} + + + )} + + ))} {showPagination && ( )} diff --git a/src/pages/Assignments/Assignment.tsx b/src/pages/Assignments/Assignment.tsx index b178162a..649d4adb 100644 --- a/src/pages/Assignments/Assignment.tsx +++ b/src/pages/Assignments/Assignment.tsx @@ -1,16 +1,16 @@ -import { Button, Col, Container, Row } from "react-bootstrap"; -import { Outlet, useLocation, useNavigate } from "react-router-dom"; +import { Row as TRow } from "@tanstack/react-table"; +import Table from "components/Table/Table"; +import useAPI from "hooks/useAPI"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { Button, Col, Container, Row } from "react-bootstrap"; +import { BsFileText } from "react-icons/bs"; import { useDispatch, useSelector } from "react-redux"; +import { Outlet, useLocation, useNavigate } from "react-router-dom"; +import { alertActions } from "store/slices/alertSlice"; +import { RootState } from "../../store/store"; +import { IAssignmentResponse } from "../../utils/interfaces"; import { assignmentColumns as ASSIGNMENT_COLUMNS } from "./AssignmentColumns"; -import { BsFileText } from "react-icons/bs"; import DeleteAssignment from "./AssignmentDelete"; -import { IAssignmentResponse } from "../../utils/interfaces"; -import { RootState } from "../../store/store"; -import { Row as TRow } from "@tanstack/react-table"; -import Table from "components/Table/Table"; -import { alertActions } from "store/slices/alertSlice"; -import useAPI from "hooks/useAPI"; const Assignments = () => { @@ -31,6 +31,25 @@ const Assignments = () => { data?: IAssignmentResponse; }>({ visible: false }); + /** + * At this moment the backend has deviated substantially from what the frontend + * assignment creator provides. However, the backend also does not accept an instructor_id + * when creating an assignment which is a required field so there is no way to create an + * assignment using the frontend. This function is a placeholder to generate fake assignments + * until the backend is updated to allow for the creation of assignments. + */ + const generateFakeAssignments = useCallback(() => { + return Array.from({ length: 10 + Math.floor(Math.random() * 10) }, (_, idx) => ({ + id: idx + 1000, + name: "Fake Assignment " + (idx + 1), + description: "This is a fake assignment", + course_id: idx + 999, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + courseName: "Fake Course " + (idx + 1), + })); + }, []); + const fetchData = useCallback(async () => { try { @@ -58,9 +77,11 @@ const Assignments = () => { const course = coursesResponse.data.find((c: any) => c.id === assignment.course_id); return { ...assignment, courseName: course ? course.name : 'Unknown' }; }); + + const fakeAssignments = generateFakeAssignments(); + mergedData = mergedData.concat(fakeAssignments); } - - + // Error alert useEffect(() => { @@ -118,7 +139,6 @@ const Assignments = () => { columns={tableColumns} columnVisibility={{ id: false, - }} /> diff --git a/src/pages/Assignments/AssignmentColumns.tsx b/src/pages/Assignments/AssignmentColumns.tsx index e5a4b6f7..368a5945 100644 --- a/src/pages/Assignments/AssignmentColumns.tsx +++ b/src/pages/Assignments/AssignmentColumns.tsx @@ -10,9 +10,6 @@ export const assignmentColumns = (handleEdit: Fn, handleDelete: Fn) => [ columnHelper.accessor("name", { header: "Name", }), - columnHelper.accessor("courseName", { - header: "Course Name", - }), columnHelper.accessor("created_at", { header: "Creation Date", }), @@ -41,4 +38,4 @@ export const assignmentColumns = (handleEdit: Fn, handleDelete: Fn) => [ ), }), -]; \ No newline at end of file +]; diff --git a/src/pages/Courses/Course.tsx b/src/pages/Courses/Course.tsx index d1e4db04..cc64b06d 100644 --- a/src/pages/Courses/Course.tsx +++ b/src/pages/Courses/Course.tsx @@ -12,14 +12,15 @@ import { ICourseResponse, ROLE } from "../../utils/interfaces"; import { courseColumns as COURSE_COLUMNS } from "./CourseColumns"; import CopyCourse from "./CourseCopy"; import DeleteCourse from "./CourseDelete"; +import CourseAssignments from "./CourseAssignments"; import { formatDate, mergeDataAndNames } from "./CourseUtil"; -// Courses Component: Displays and manages courses, including CRUD operations. - /** * @author Atharva Thorve, on December, 2023 * @author Mrityunjay Joshi on December, 2023 + * @author Mark Feng on December, 2024 */ + const Courses = () => { const { error, isLoading, data: CourseResponse, sendRequest: fetchCourses } = useAPI(); const { data: InstitutionResponse, sendRequest: fetchInstitutions } = useAPI(); @@ -95,15 +96,36 @@ const Courses = () => { [] ); + const renderSubComponent = useCallback(({ row }: { row: TRow }) => { + return ( + + ); + }, []); + const tableColumns = useMemo( () => COURSE_COLUMNS(onEditHandle, onDeleteHandle, onTAHandle, onCopyHandle), [onDeleteHandle, onEditHandle, onTAHandle, onCopyHandle] ); let tableData = useMemo( - () => (isLoading || !CourseResponse?.data ? [] : CourseResponse.data), - [CourseResponse?.data, isLoading] - ); + () => { + if (isLoading || !CourseResponse?.data) { + // Generate fake courses if no data + return Array.from({ length: 5 }, (_, idx) => ({ + id: 1000 + idx, + name: `Course ${idx + 1} - Software Engineering`, + institution: { id: 1, name: "Test University" }, + created_at: new Date(Date.now() - Math.random() * 10000000000).toISOString(), + updated_at: new Date().toISOString(), + })); + } + return CourseResponse.data; + }, + [CourseResponse?.data, isLoading] +); const institutionData = useMemo( () => (isLoading || !InstitutionResponse?.data ? [] : InstitutionResponse.data), @@ -117,9 +139,6 @@ const Courses = () => { created_at: formatDate(item.created_at), updated_at: formatDate(item.updated_at), })); - - // Render the Courses component - return ( <> @@ -156,6 +175,8 @@ const Courses = () => { id: false, institution: auth.user.role === ROLE.SUPER_ADMIN.valueOf(), }} + renderSubComponent={renderSubComponent} + getRowCanExpand={() => true} /> @@ -164,4 +185,4 @@ const Courses = () => { ); }; -export default Courses; +export default Courses; \ No newline at end of file diff --git a/src/pages/Courses/CourseAssignments.test.tsx b/src/pages/Courses/CourseAssignments.test.tsx new file mode 100644 index 00000000..7a3bbdaa --- /dev/null +++ b/src/pages/Courses/CourseAssignments.test.tsx @@ -0,0 +1,72 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { BrowserRouter } from 'react-router-dom'; +import CourseAssignments from './CourseAssignments'; + +const renderWithRouter = (component: React.ReactNode) => { + return render( + + {component} + + ); +}; + +describe('CourseAssignments', () => { + const mockCourseId = 101; + const mockCourseName = 'Test Course'; + + it('renders the component correctly', () => { + renderWithRouter(); + + // Check if the course name is displayed + expect(screen.getByText(`Assignments for ${mockCourseName}`)).toBeInTheDocument(); + + // Check if the table renders + const table = screen.getByRole('table'); + expect(table).toBeInTheDocument(); + }); + + it('renders assignments in the table', () => { + renderWithRouter(); + + // Check for table rows (excluding header row) + const rows = screen.getAllByRole('row'); + expect(rows.length).toBeGreaterThan(1); // Header + assignment rows + }); + + it('triggers edit and delete actions correctly', async () => { + // Spy on console.log to check if handlers are called + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + + renderWithRouter(); + + // Find and verify buttons + const editButtons = screen.getAllByRole('button', { name: /edit/i }); + const deleteButtons = screen.getAllByRole('button', { name: /delete/i }); + + expect(editButtons).toHaveLength(4); // Adjust based on your table rows + expect(deleteButtons).toHaveLength(4); + + // Trigger clicks + await userEvent.click(editButtons[0]); + await userEvent.click(deleteButtons[0]); + + // Check exact console log outputs + const firstAssignment = { + id: expect.any(Number), + name: expect.stringContaining('Assignment 1'), + courseName: mockCourseName, + description: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + }; + + expect(consoleSpy).toHaveBeenCalledWith('Edit assignment:', expect.objectContaining(firstAssignment)); + expect(consoleSpy).toHaveBeenCalledWith('Delete assignment:', expect.objectContaining(firstAssignment)); + + // Clean up mock + consoleSpy.mockRestore(); + }); + +}); diff --git a/src/pages/Courses/CourseAssignments.tsx b/src/pages/Courses/CourseAssignments.tsx new file mode 100644 index 00000000..de148d14 --- /dev/null +++ b/src/pages/Courses/CourseAssignments.tsx @@ -0,0 +1,177 @@ +import { Row as TRow } from "@tanstack/react-table"; +import Table from "components/Table/Table"; +import React from "react"; +import { assignmentColumns as getBaseAssignmentColumns } from "../Assignments/AssignmentColumns"; +import { capitalizeFirstWord, formatDate } from "utils/dataFormatter"; + +interface ActionHandler { + icon: string; + label: string; + handler: (row: TRow) => void; + className?: string; +} + +interface CourseAssignmentsProps { + courseId: number; + courseName: string; +} + +const CourseAssignments: React.FC = ({ courseId, courseName }) => { + const actionHandlers: ActionHandler[] = [ + { + icon: "/assets/icons/edit-temp.png", + label: "Edit", + handler: (row: TRow) => { + console.log("Edit assignment:", row.original); + }, + className: "text-primary", + }, + { + icon: "/assets/icons/delete-temp.png", + label: "Delete", + handler: (row: TRow) => { + console.log("Delete assignment:", row.original); + }, + className: "text-danger", + }, + { + icon: "/assets/icons/add-participant-24.png", + label: "Add Participant", + handler: (row: TRow) => { + console.log("Add participant to assignment:", row.original); + }, + className: "text-success", + }, + { + icon: "/assets/icons/assign-reviewers-24.png", + label: "Assign Reviewers", + handler: (row: TRow) => { + console.log("Assign reviewers for:", row.original); + }, + className: "text-info", + }, + { + icon: "/assets/icons/create-teams-24.png", + label: "Create Teams", + handler: (row: TRow) => { + console.log("Create teams for:", row.original); + }, + className: "text-primary", + }, + { + icon: "/assets/icons/view-review-report-24.png", + label: "View Review Report", + handler: (row: TRow) => { + console.log("View review report:", row.original); + }, + className: "text-secondary", + }, + { + icon: "/assets/icons/view-scores-24.png", + label: "View Scores", + handler: (row: TRow) => { + console.log("View scores:", row.original); + }, + className: "text-info", + }, + { + icon: "/assets/icons/view-submissions-24.png", + label: "View Submissions", + handler: (row: TRow) => { + console.log("View submissions:", row.original); + }, + className: "text-secondary", + }, + { + icon: "/assets/icons/copy-temp.png", + label: "Copy Assignment", + handler: (row: TRow) => { + console.log("Copy assignment:", row.original); + }, + className: "text-success", + }, + { + icon: "/assets/icons/export-temp.png", + label: "Export", + handler: (row: TRow) => { + console.log("Export assignment:", row.original); + }, + className: "text-primary", + }, + ]; + + // TODO: update it with actual api calls to get assignment list + const generateFakeAssignments = () => { + const numAssignments = 3 + Math.floor(Math.random() * 3); + return Array.from({ length: numAssignments }, (_, idx) => ({ + id: parseInt(`${courseId}${idx}`), + name: `Assignment ${idx + 1}`, + description: "This is a fake assignment", + created_at: new Date(Date.now() - Math.random() * 10000000000).toISOString(), + updated_at: new Date().toISOString(), + })); + }; + + const getAssignmentColumns = (actions: ActionHandler[]) => { + const baseColumns = getBaseAssignmentColumns( + () => {}, + () => {} + ).filter((col) => !["edit", "delete", "actions"].includes(String(col.id))); + + const actionsColumn = { + id: "actions", + header: "Actions", + cell: ({ row }: { row: TRow }) => ( +
+ {actions.map((action, index) => ( + + ))} +
+ ), + }; + + return [...baseColumns, actionsColumn]; + }; + + const assignments = generateFakeAssignments(); + const columns = getAssignmentColumns(actionHandlers); + + // Format all heading data fields. + const filteredColumns = columns.map(({ header, ...rest }) => ({ + ...rest, + header: capitalizeFirstWord(header as string), + })); + + // Format some assignment data fields. + const filteredAssignments = assignments.map(({ name, created_at, updated_at, ...rest }) => ({ + ...rest, + name: capitalizeFirstWord(name), + created_at: formatDate(created_at), // Format 'created_at' date + updated_at: formatDate(updated_at), // Format 'updated_at' date + })); + + return ( +
+
Assignments for {courseName}
+ + + ); +}; + +export default CourseAssignments; diff --git a/src/pages/StudentTasks/StudentTaskDetail.module.css b/src/pages/StudentTasks/StudentTaskDetail.module.css new file mode 100644 index 00000000..53314624 --- /dev/null +++ b/src/pages/StudentTasks/StudentTaskDetail.module.css @@ -0,0 +1,187 @@ +.container { + font-family: Arial, sans-serif; + padding: 0.5rem 1.5rem; + display: block; +} + +/* Header Section */ +.header { + box-sizing: border-box; +} + +.header h1 { + font-size: 36px; + font-weight: 500; + line-height: 1.1; + color: inherit; + margin-top: 20px; + margin-bottom: 20px; + padding-left: 0; +} + +.flash_note { + display: flex; + align-items: center; + color: green; + border: 2px solid #0a0; + background-color: #8f8; + padding: 5px 20px 5px; + margin-bottom: 15px; +} + +.programLink { + color: #0066cc; + text-decoration: none; +} + +.programLink:hover { + text-decoration: underline; +} + +.emailLink { + color: #0066cc; + text-decoration: none; + font-weight: 500; + padding: 8px 16px; + border: 1px solid #0066cc; + border-radius: 4px; + font-size: 14px; +} + +.emailLink:hover { + background-color: #f0f7ff; +} + +/* Task Links Section */ +.taskLinks { + margin-bottom: 30px; + padding: 1rem; +} + +.taskItem { + font-size: 14px; + line-height: 1.5; +} + +.taskMainLink { + color: #28a745; /* Green for main links */ + text-decoration: none; + font-weight: 500; +} + +.taskMainLink:hover { + text-decoration: underline; + color: #986633; +} + +.taskDescription { + color: #333; /* Black for descriptions */ +} + +.alternativeView { + color: #28a745; /* Green for alternative view */ + text-decoration: none; + margin-left: 4px; +} + +.alternativeView:hover { + text-decoration: underline; + color: #986633; +} + +/* Timeline Section */ +.timelineContainer { + margin: 40px 0; +} + +.timelineDates { + display: flex; + justify-content: space-between; + font-size: 16px; + color: #555; + align-items: center; + margin-bottom: 12px; +} + +.timelineVisual { + position: relative; + height: 30px; + margin: 15px 0; +} + +.timelineLine { + position: absolute; + top: 50%; + left: 0; + right: 0; + background-color: #dc3545; + transform: translateY(-50%); + height: 2px; + width: 100%; + transform: translateY(-50%); +} + +.timelineDots { + display: flex; + justify-content: space-between; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 100%; +} + +.dot { + width: 25px; + height: 25px; + background-color: #dc3545; + border-radius: 50%; + margin-top: 7px; +} + +.timelineDeadlines { + display: flex; + justify-content: space-between; + margin-top: 20px; +} + +.deadlineLink { + color: #0066cc; + text-decoration: none; + font-weight: bold; + text-align: center; + flex: 1; + font-size: 16px; +} + +.deadlineLink:hover { + text-decoration: underline; +} + +/* Footer Section */ +.footer { + display: flex; + justify-content: center; + padding: 20px 0; +} + +/* Footer link styling */ +.clickableLink { + margin: 0 10px; + color: #986633; + text-decoration: none; + .to_right { + float: right; + } +} + +.link_to_right { + float: right; +} + +/* Footer link hover styling */ +.clickableLink:hover { + color: #000000; + text-decoration: underline; +} + diff --git a/src/pages/StudentTasks/StudentTaskDetail.tsx b/src/pages/StudentTasks/StudentTaskDetail.tsx new file mode 100644 index 00000000..5940f03f --- /dev/null +++ b/src/pages/StudentTasks/StudentTaskDetail.tsx @@ -0,0 +1,359 @@ +import React, { useState, useEffect } from "react"; +import { useParams, Link, useLocation } from "react-router-dom"; +import styles from "./StudentTaskDetail.module.css"; +import axiosClient from "utils/axios_client"; + +// Define interfaces for type safety +interface Deadline { + id: number; + date: string; + description: string; +} + +interface TaskData { + assignment: string; + badges: boolean; + course: string; + currentStage: string; + deadlines: Deadline[]; + id: number; + publishingRights: boolean; + reviewGrade: string; + stageDeadline: string; + topic: string; +} + +interface StateData { + task: TaskData; +} + +const StudentTaskDetail: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const location = useLocation(); + const stateData = location.state as StateData; + + // Log the entire stateData object + console.log('Complete stateData:', stateData); + + // Access data through the task property + const taskData = stateData?.task || {}; + + // Extract individual properties with fallbacks + const assignment = taskData.assignment || "Unknown Assignment"; + const course = taskData.course || "Unknown Course"; + const current_stage = taskData.currentStage || "Not Started"; + const deadlines = taskData.deadlines || []; + const topic = taskData.topic || "No Topic"; + const permission_granted = taskData.publishingRights || false; + const stage_deadline = taskData.stageDeadline || null; + const review_grade = taskData.reviewGrade || "N/A"; + + // Log the extracted properties + console.log({ + assignment, + course, + current_stage, + deadlines, + topic, + permission_granted, + stage_deadline, + review_grade + }); + + // Log deadlines as a table for better visualization + console.table(deadlines); + + // Helper function to determine stage status + const getStageStatus = (index: number): "completed" | "current" | "pending" => { + const currentStageIndex = deadlines.findIndex(deadline => + deadline.description.toLowerCase().includes(current_stage.toLowerCase())); + + if (currentStageIndex === -1) { + // If current stage not found in deadlines, use date comparison + const today = new Date("2025-04-19T18:01:00"); // Current date from your context + const deadlineDate = new Date(deadlines[index].date); + + if (deadlineDate < today) return "completed"; + if (index === 0) return "current"; // Default to first stage as current if no match + return "pending"; + } + + if (index < currentStageIndex) return "completed"; + if (index === currentStageIndex) return "current"; + return "pending"; + }; + + // Your existing API calls + const [tasks, setTasks] = useState(null); + useEffect(() => { + const fetchStudentTasks = async () => { + try { + const response = await axiosClient.get(`/student_tasks/list`); + setTasks(response.data); + console.log("hello", response.data); + } catch (error) { + console.error("Error fetching student tasks:", error); + } + }; + + fetchStudentTasks(); + }, []); + + const [assignments, setAssignments] = useState(null); + useEffect(() => { + const fetchAssignments = async () => { + try { + const response = await axiosClient.get(`/assignments`); + setAssignments(response.data); + console.log("hello assignment", response.data); + } catch (error) { + console.error("Error fetching assignments:", error); + } + }; + + fetchAssignments(); + }, []); + + return ( +
+
+

+ Submit or review work for{" "} + + {assignment} + +

+
+ +
+ Next: Click the activity you wish to perform on the assignment titled:{" "} + {assignment} +
+ + + Send Email To Reviewers + + +
+
    +
  • + + Your team + + (View and manage your team) +
  • +
  • + + Your work + + (View your work) +
  • +
  • + + Other's work + + (Give feedback to others on their work) +
  • +
  • + + Your scores + + (View feedback on your work) + + Alternative View + +
  • +
  • + + Change your handle + + + {" "} + (Provide a different handle for this assignment) + +
  • +
+
+ + + {/* Timeline section */} +
+
+ {/* Dates */} +
+ {deadlines.map((deadline: Deadline) => ( + + {new Date(deadline.date).toLocaleDateString('en-US', { + weekday: 'short', + month: 'short', + day: 'numeric', + year: 'numeric', + })} + + ))} +
+ + {/* Visual timeline */} +
+ {/* Calculate the gradient stops based on deadline dates */} + {(() => { + const today = new Date(); + const totalDeadlines = deadlines.length; + + // Find the index where future dates begin + let futureStartIndex = deadlines.findIndex(d => new Date(d.date) > today); + if (futureStartIndex === -1) futureStartIndex = totalDeadlines; // All dates are in the past + + // Calculate the percentage where the color should change + const changePoint = futureStartIndex / totalDeadlines * 100; + + // Create the gradient style + const gradientStyle = { + background: `linear-gradient(to right, + var(--timeline-past-color, #ff0000) 0%, + var(--timeline-past-color, #ff0000) ${changePoint}%, + #D6DCE0 ${changePoint}%, + #D6DCE0 100%)` + }; + + return ( +
+ ); + })()} + +
+ {deadlines.map((deadline: Deadline, index: number) => ( +
new Date() ? 'white' : undefined, + border: new Date(deadline.date) > new Date() ? '1px solid #E2E2E2' : undefined + }} + >
+ ))} +
+
+ +{/* Deadlines/Links */} +
+ {deadlines.map((deadline: Deadline) => { + // Check if the deadline is a Submission or Review deadline + const isNonClickable = + deadline.description === "Submission deadline" || + deadline.description === "Review deadline"; + + // Conditionally render either a Link or a span + return isNonClickable ? ( + + {deadline.description} + + ) : ( + + {deadline.description} + + ); + })} +
+
+
+ + {/* Footer section */} + +
+ + Back + +
+
+
+ + Help + + + Papers on Expertiza + +
+
+
+ ); +}; + +export default StudentTaskDetail; diff --git a/src/pages/StudentTasks/StudentTasks.module.css b/src/pages/StudentTasks/StudentTasks.module.css new file mode 100644 index 00000000..54e92df3 --- /dev/null +++ b/src/pages/StudentTasks/StudentTasks.module.css @@ -0,0 +1,179 @@ +/* Base container styling */ +.container { + font-family: Arial, sans-serif; + margin: 20px; +} + +/* Header 1 styling */ +h1 { + text-align: left; + padding-top: 5px; + padding-left: 15px; + margin-bottom: 0px; + padding-bottom: 0px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.428571429; + color: #333333; +} + +/* Table base styling */ +table { + width: 100%; + border-collapse: collapse; + margin-top: 20px; +} + +/* Table, th, and td shared border styling */ +table, +th, +td { + border: 1px solid #ddd; +} + +/* Table header styling */ +th { + background-color: #f8f8f8; + color: #333; + text-align: left; + padding: 8px; +} + +/* Table data cell styling */ +td { + padding: 8px; + text-align: left; +} + +/* First child of th and td border styling */ +th:first-child, +td:first-child { + border-left: none; +} + +/* Last child of th and td border styling */ +th:last-child, +td:last-child { + border-right: none; +} + +/* Table row striping for even rows */ +tr:nth-child(even) { + background-color: #f2f2f2; +} + +/* Badge info icon and publishing rights checkbox styling */ +.badge-info-icon, +.publishing-rights-checkbox { + cursor: pointer; + margin-left: 5px; +} + +/* Checked state styling for publishing rights checkbox */ +.publishing-rights-checkbox:checked { + accent-color: #009688; +} + +/* Table header row font styling */ +thead tr { + font-weight: bold; +} + +/* Status indicator icons styling */ +.status-indicator { + color: #009688; + margin-left: 5px; +} + +/* Disabled state styling for checkboxes */ +input[type="checkbox"][disabled] { + opacity: 0.6; + cursor: not-allowed; +} + +/* Information icon styling in table */ +.info-icon { + font-style: normal; + color: #017bff; + cursor: help; +} + +/* Side information section styling */ +.side-info { + color: #333; + padding: 10px 0; +} + +/* Number styling in side information section */ +.side-info .number { + font-weight: bold; + color: #d9534f; /* Red color for the number badge */ +} + +/* Page layout styling */ +.pageLayout { + display: flex; + margin: 16px; +} + +/* Sidebar styling */ +.sidebar { + width: 250px; + margin-right: 20px; + padding-top: 20px; +} + +/* Main content area styling */ +.mainContent { + flex-grow: 1; + overflow: hidden; +} + +/* Header below pageLayout styling */ +.header { + margin-bottom: 20px; +} + +/* Tasks table styling */ +.tasksTable { + width: 100%; +} + +/* Page assignments styling */ +.assignments-page { + font-family: 'Arial', sans-serif; +} + +/* Title in assignments page styling */ +.assignments-title { + color: #333; + text-align: left; + padding: 20px; + font-size: 24px; +} + +/* Footer section styling */ +.footer { + display: flex; + justify-content: center; + padding: 20px 0; +} + +/* Footer link styling */ +.footerLink { + margin: 0 10px; + color: #986633; + text-decoration: none; +} + +/* Footer link hover styling */ +.footerLink:hover { + color: #000000; + text-decoration: underline; +} + +/* Center checkbox in table data cell styling */ +.centerCheckbox { + text-align: center; + vertical-align: middle; +} diff --git a/src/pages/StudentTasks/StudentTasks.tsx b/src/pages/StudentTasks/StudentTasks.tsx new file mode 100644 index 00000000..abd0123e --- /dev/null +++ b/src/pages/StudentTasks/StudentTasks.tsx @@ -0,0 +1,213 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { Link } from "react-router-dom"; +import styles from "./StudentTasks.module.css"; +import StudentTasksBox from "./StudentTasksBox"; +import testData from "./testData.json"; +import { CellContext } from "@tanstack/react-table"; // or the correct import for your table library +import Table from "components/Table/Table"; +import { formatDate, capitalizeFirstWord } from "utils/dataFormatter"; +import axiosClient from "utils/axios_client"; +import ToolTip from "../../components/ToolTip"; + +// Define the types for a single task and the associated course +type Task = { + id: number; + assignment: string; + course: string; + topic: string; + currentStage: string; + reviewGrade: string; + badges: string | boolean; + stageDeadline: string; + publishingRights: boolean; +}; + +// Main functional component for Student Tasks +const StudentTasks: React.FC = () => { + // Store the studentTaskData + const [studentTasksData, setStudentTasksData] = useState([]); + // State to hold tasks + const [tasks, setTasks] = useState([]); + // Team test data + const studentsTeamedWith = testData.studentsTeamedWith; + + // Fetch student tasks from the backend when the component mounts + useEffect(() => { + const fetchStudentTasks = async () => { + try { + const response = await axiosClient.get(`/student_tasks/list`); + setStudentTasksData(response.data); + } catch (error) { + console.error("Error fetching student tasks:", error); + } + }; + + fetchStudentTasks(); + }, []); + + // Parse and update tasks whenever studentTasksData changes + useEffect(() => { + setTasks(parseStudentTasks(studentTasksData)); + }, [studentTasksData]); + + /** + * Converts a raw list of student task data into a structured array of Task objects + */ + function parseStudentTasks(rawList: any[]): Task[] { + return rawList.map((item) => { + const participant = item.participant || {}; + const course = item.course || {}; + return { + id: participant.id, + assignment: item.assignment ?? "N/A", + course: course ?? "CSC 517", // Not present in data, defaulting + topic: participant.topic ?? "N/A", + currentStage: item.current_stage ?? participant.current_stage ?? "N/A", + reviewGrade: item.review_grade ?? "N/A", + badges: item.badges ?? false, // Adjust if you have badges logic elsewhere + stageDeadline: item.stage_deadline ?? participant.stage_deadline ?? "", + publishingRights: participant.permission_granted ?? false, + deadlines: item.deadlines, // Additional fields for StudentTaskDetails + }; + }); + } + + /** + * Extracts assignment names and due dates from student task data. + */ + function extractAssignments(tasks: any[]) { + return tasks.map((task) => ({ + name: task.assignment, + dueDate: task.stageDeadline.split("T")[0], + })); + } + + /** + * Handle toggle publishing rights checkbox + */ + const togglePublishingRights = useCallback((id: number) => { + setTasks((prevTasks) => + prevTasks.map((task) => + task.id === id ? { ...task, publishingRights: !task.publishingRights } : task + ) + ); + }, []); + + const showBadges = tasks.some((task) => task.badges); + + /** + * Extracts table columns + */ + const filteredColumns = [ + { + accessorKey: "assignment", + header: "Assignment", + cell: (info: CellContext) => { + const id = info.row.original.id; + const matchedTask = tasks.find((task) => task.id === id); + return ( + + {info.getValue()} + + ); + }, + }, + { accessorKey: "course", header: "Course" }, + { accessorKey: "topic", header: "Topic" }, + { accessorKey: "currentStage", header: "Current Stage" }, + { + accessorKey: "reviewGrade", + header: "Review Grade", + cell: (info: CellContext) => + info.getValue() === "N/A" ? ( + "NA" + ) : ( + + ), + }, + ...(showBadges ? [{ accessorKey: "badges", header: "Badges" }] : []), + { + accessorKey: "stageDeadline", + header: "Stage Deadline", + comment: "You can change 'Preferred Time Zone' in 'Profile' in the banner.", + }, + { + accessorKey: "publishingRights", + header: "Publishing Rights", + cell: (info: CellContext) => ( + togglePublishingRights(Number(info.row.original.id))} + /> + ), + comment: "Grant publishing rights", + }, + ].map(({ header, ...rest }) => ({ + ...rest, + header: capitalizeFirstWord(header as string), + })); + + /** + * Extracts related table records + */ + const filteredAssignments = tasks.map((task) => ({ + ...task, + topic: capitalizeFirstWord(task.topic) || "-", + course: capitalizeFirstWord(task.course), + reviewGrade: task.reviewGrade || "N/A", + badges: task.badges || "", + stageDeadline: formatDate(task.stageDeadline) || "No deadline", + publishingRights: task.publishingRights || false, + })); + + // Component render method + return ( +
+

Assignments

+
+ + + {/* Table section */} +
+
+
+ + + + + {/* Footer section */} +
+ + Help + + + Papers on Expertiza + +
+ + ); +}; + +// Export the component for use in other parts of the application +export default StudentTasks; \ No newline at end of file diff --git a/src/pages/StudentTasks/StudentTasksBox.module.css b/src/pages/StudentTasks/StudentTasksBox.module.css new file mode 100644 index 00000000..3db98a78 --- /dev/null +++ b/src/pages/StudentTasks/StudentTasksBox.module.css @@ -0,0 +1,120 @@ +/* Stripes for odd rows in a table within .student-tasks */ +.student-tasks .table-striped>tbody>tr:nth-of-type(odd)>td, +.student-tasks .table-striped>tbody>tr:nth-of-type(odd)>th { + background-color: #ffffff; + --bs-table-bg-type: none +} + +/* Stripes for even rows in a table within .student-tasks */ +.student-tasks .table-striped>tbody>tr:nth-of-type(even)>td, +.student-tasks .table-striped>tbody>tr:nth-of-type(even)>th { + background-color: #f2f2f2; + --bs-table-bg-type: none; +} + +/* Styling for task boxes */ +.taskbox { + padding: 5px; + margin-bottom: 39px; + border: 1px dashed #999999; + float: left; + font-size: 12px; + background: none repeat scroll 0pt 0pt #fafaea; + width: 100%; +} + +/* Styling for the task number indicator */ +.tasknum { + color: #FFFFFF; + background-color: #B73204; +} + +/* Styling for the revision number indicator */ +.revnum { + color: #FFFFFF; + background-color: #999999; +} + +/* Styling for notification links */ +.notification a { + color:#0066CC +} + +/* Layout for the page containing the student tasks */ +.pageLayout { + display: flex; + margin: 16px; +} + +/* Fixed-width sidebar styling */ +.sidebar { + width: 200px; /* Width of the sidebar */ + margin-right: 20px; /* Spacing between sidebar and main content */ +} + +/* Main content area that grows to fill the space */ +.mainContent { + flex-grow: 1; + overflow: hidden; /* In case the content is too wide */ +} + +/* Styling for the page header */ +.header { + margin-bottom: 20px; /* Space below the header */ +} + +/* Full-width table styling */ +.tasksTable { + width: 100%; /* Full width of the main content area */ + /* Add more styling for your table */ +} + +/* Margin styling for sections */ +.section { + margin-bottom: 20px; /* Space between sections */ +} + +/* Header styling within sections */ +.section-header { + font-size: 18px; /* Larger font size for visibility */ + font-weight: bold; /* Bold text for section headers */ + color: #333; /* Darker text for better readability */ + margin-bottom: 10px; /* Space below section header */ +} + +/* Styling for individual items within a section */ +.section-item { + margin-left: 20px; /* Indent for items in the list */ + margin-bottom: 5px; /* Space between items */ + color: #555; /* Slightly lighter text for items */ +} + +/* Standard badge styling */ +.badge { + display: inline-block; + padding: 0.25em 0.4em; + font-size: 75%; + font-weight: 700; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0; + padding: 1; + background-color: #a52a2a; + color: white; +} + +/* Grey badge variant */ +.greyBadge{ + padding: 0.25em 0.4em; + font-size: 75%; + font-weight: 700; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0; + background-color: rgb(159, 156, 156); + color: white; +} diff --git a/src/pages/StudentTasks/StudentTasksBox.tsx b/src/pages/StudentTasks/StudentTasksBox.tsx new file mode 100644 index 00000000..ef489579 --- /dev/null +++ b/src/pages/StudentTasks/StudentTasksBox.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import styles from './StudentTasksBox.module.css'; + + type Revision = { + name: string; + dueDate: string; + }; + + //Students teamed with data structure + type StudentsTeamedWith = { + [semester: string]: string[]; + }; + + // interface for Student task box data + interface StudentTasksBoxProps { + revisions: Revision[]; + studentsTeamedWith: StudentsTeamedWith; + } + + const StudentTasksBox: React.FC = ({ revisions, studentsTeamedWith }) => { + + let totalStudents = 0; + for (const semester in studentsTeamedWith) { + totalStudents += studentsTeamedWith[semester].length; + } + + // Function to calculate the number of days left until the due date + const calculateDaysLeft = (dueDate: string) => { + const today = new Date(); + const due = new Date(dueDate); + const timeDiff = due.getTime() - today.getTime(); + const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24)); + return daysDiff > 0 ? daysDiff : 0; + }; + + // Find the revisions that have not done yet based on the due date + const revisedTasks = revisions.filter(revisions => calculateDaysLeft(revisions.dueDate) > 0); + +// HTML for student task box + return ( +
+
+ 0   + Tasks not yet started +
+ + {/* Revisions section (remains empty since revisions array is empty) */} +
+ {revisedTasks.length}   + Revisions + {revisedTasks.map((task, index) => { + const daysLeft = calculateDaysLeft(task.dueDate); + return ( +
+ » {task.name} ({daysLeft} day{daysLeft !== 1 ? 's' : ''} left) +
+ ); + })} +
+ + + {/* Students who have teamed with you section */} +
+ {totalStudents}   + Students who have teamed with you +
+ {Object.entries(studentsTeamedWith).map(([semester, students], index) => ( +
+ {semester} + {students.length} + {students.map((student, studentIndex) => ( +
+ » {student} +
+ ))} +
+ ))} +
+ ); +}; + +// Exporting the component for use in other parts of the application +export default StudentTasksBox; diff --git a/src/pages/StudentTasks/testData.json b/src/pages/StudentTasks/testData.json new file mode 100644 index 00000000..d79c8199 --- /dev/null +++ b/src/pages/StudentTasks/testData.json @@ -0,0 +1,173 @@ + +{ + "participantTasks": [ + { + "id": 1, + "assignment": "Program 1", + "course": "CSC 517", + "topic": "Ruby", + "current_stage": "In progress", + "review_grade": { + "comment": "Score: 30/100 \nComment: 4 reviews x 10 pts/review x 75% = 30 points \nA few explanationtion were given for the scores. The explanations (-25%)" + }, + "badges": false, + "stage_deadline": "2024-02-26 12:00:00 -0500", + "publishing_rights": true, + "participant_id": 1 + }, + { + "id": 2, + "assignment": "Program 2", + "course": "CSC 517", + "topic": "Rails", + "current_stage": "In progress", + "review_grade": "N/A", + "badges": false, + "stage_deadline": "2024-02-26 12:00:00 -0500", + "publishing_rights": false, + "participant_id": 2 + }, + { + "id": 3, + "assignment": "Program 3", + "course": "CSC 517", + "topic": "Git", + "current_stage": "In progress", + "review_grade": "N/A", + "badges": false, + "stage_deadline": "2024-02-26 12:00:00 -0500", + "publishing_rights": false, + "participant_id": 1 + }, + { + "id": 4, + "assignment": "Program 4", + "course": "CSC 517", + "topic": "Reimplementation Backend", + "current_stage": "In progress", + "review_grade": { + "comment": "Score: 40/100 \nComment: 4 reviews x 10 pts/review x 100% = 40 points. The comments were detailed and provide good explanations of the given score. \nGood job!" + }, + "badges": false, + "stage_deadline": "2024-02-26 12:00:00 -0500", + "publishing_rights": true, + "participant_id": 1 + + }, + { + "id": 5, + "assignment": "Program 5", + "course": "CSC 517", + "topic": "Reimplementation Frontend", + "current_stage": "Finished", + "review_grade": "N/A", + "badges": false, + "stage_deadline": "2024-02-26 12:00:00 -0500", + "publishing_rights": false, + "participant_id": 1 + }, + { + "id": 6, + "assignment": "OSS Program 1", + "course": "CSC 517", + "topic": "Reimplementation Frontend", + "current_stage": "Finished", + "review_grade": "N/A", + "badges": false, + "stage_deadline": "2024-02-26 12:00:00 -0500", + "publishing_rights": false, + "participant_id": 1 + }, + { + "id": 7, + "assignment": "OSS Program 2", + "course": "CSC 517", + "topic": "Reimplementation Frontend", + "current_stage": "Finished", + "review_grade": "N/A", + "badges": false, + "stage_deadline": "2024-02-26 12:00:00 -0500", + "publishing_rights": false, + "participant_id": 1 + }, + { + "id": 8, + "assignment": "Program 5", + "course": "CSC 517", + "topic": "Reimplementation Frontend", + "current_stage": "Finished", + "review_grade": "N/A", + "badges": false, + "stage_deadline": "2024-02-26 12:00:00 -0500", + "publishing_rights": true, + "participant_id": 1 + }, + { + "id": 9, + "assignment": "Program 5", + "course": "CSC 517", + "topic": "Reimplementation Frontend", + "current_stage": "Finished", + "review_grade": "N/A", + "badges": false, + "stage_deadline": "2024-02-26 12:00:00 -0500", + "publishing_rights": true, + "participant_id": 1 + } + ], + "courses": [ + { + "id": 1, + "name": "Course3", + "directory_path": "/path/to/course_files", + "info": null, + "private": false, + "created_at": "2024-04-21T23:46:38.633Z", + "updated_at": "2024-04-21T23:46:38.633Z", + "instructor_id": 2, + "institution_id": 1 + }, + { + "id": 4, + "name": "Course4", + "directory_path": "/path/to/course_files", + "info": null, + "private": false, + "created_at": "2024-04-21T23:46:38.633Z", + "updated_at": "2024-04-21T23:46:38.633Z", + "instructor_id": 2, + "institution_id": 1 + } + + ], + + "dueTasks": [ + + ], + "revisions": [ + { + "name": "Program 1", + "dueDate": "2024-03-18" + }, + { + "name": "Program 2", + "dueDate": "2024-03-18" + }, + { + "name": "OSS project", + "dueDate": "2024-03-18" + }, + { + "name": "OSS project 2", + "dueDate": "2024-03-25" + }, + { + "name": "OSS project 3", + "dueDate": "2024-04-28" + } + ], + "studentsTeamedWith": { + "CSC/ECE 517, Fall 2024": ["teammate one", "teammate two", "teammate three", "teammate four","teammate five", "teammate six", "teammate seven"] + }, + "current_user_id": 1 +} \ No newline at end of file diff --git a/src/pages/ViewTeamGrades/grades.scss b/src/pages/ViewTeamGrades/grades.scss index e422e64f..f80b9564 100644 --- a/src/pages/ViewTeamGrades/grades.scss +++ b/src/pages/ViewTeamGrades/grades.scss @@ -231,10 +231,8 @@ .container { display: flex; - justify-content: space-between; - /* Adjust as needed */ - width: 80%; - /* Ensure the container takes up the full width */ + justify-content: space-between; /* Adjust as needed */ + width: 100%; /* Ensure the container takes up the full width */ } diff --git a/src/utils/dataFormatter.ts b/src/utils/dataFormatter.ts new file mode 100644 index 00000000..7584dc19 --- /dev/null +++ b/src/utils/dataFormatter.ts @@ -0,0 +1,28 @@ +/** + * Capitalizes the first letter of a sentence and converts the rest to lowercase. + * + * @param str - The input string to capitalize. + * @returns A string with the first letter capitalized and the rest in lowercase. + */ +export const capitalizeFirstWord = (str: string) => { + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); +}; + +/** + * Formats a date string into a readable format (e.g., "Oct 12, 2023, 3:45 PM"). + * + * @param dateString - The input date string. + * @returns A formatted date string in "MMM DD, YYYY, hh:mm AM/PM" format. + */ +export const formatDate = (dateString: string): string => { + const date = new Date(dateString); + const options: Intl.DateTimeFormatOptions = { + month: "short", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "numeric", + hour12: true, + }; + return date.toLocaleString("en-US", options); +}; diff --git a/tsconfig.json b/tsconfig.json index 885fce5e..ed46d9dc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,27 +1,27 @@ -{ - "compilerOptions": { - "target": "es6", - "lib": [ - "dom", - "dom.iterable", - "esnext", - ], - "baseUrl": "src", - "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx" - }, - "include": [ - "src" - ] -} +{ + "compilerOptions": { + "target": "es6", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "baseUrl": "src", + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve" + }, + "include": [ + "src" + ] +}