diff --git a/package-lock.json b/package-lock.json index 3ef4561e..aa660f4c 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,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", @@ -5760,9 +5765,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.9", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz", + "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } }, "node_modules/check-types": { "version": "11.2.3", @@ -16603,6 +16614,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/src/App.tsx b/src/App.tsx index 27736ba3..0265b683 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,7 +23,8 @@ 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 EditQuestionnaire from "pages/EditQuestionnaire/Questionnaire"; +import ManageQuestionnaire from "pages/Questionnaire/questionnaire"; import Courses from "pages/Courses/Course"; import CourseEditor from "pages/Courses/CourseEditor"; import { loadCourseInstructorDataAndInstitutions } from "pages/Courses/CourseUtil"; @@ -57,7 +58,11 @@ function App() { }, { path: "edit-questionnaire", - element: } />, + element: } />, + }, + { + path: "questionnaire", + element: } />, }, { path: "assignments/edit/:id/createteams", @@ -282,14 +287,19 @@ function App() { }, ], }, + { + path: "edit-questionnaire", + element: , + }, { path: "questionnaire", - element: , + element: , }, ], }, { path: "*", element: }, - { path: "questionnaire", element: }, // Added the Questionnaire route + { path: "edit-questionnaire", element: }, // Added the Questionnaire route + { path: "questionnaire", element: }, // Added the Questionnaire route ], }, ]); diff --git a/src/assets/icons/Copy-icon-24.png b/src/assets/icons/Copy-icon-24.png new file mode 100644 index 00000000..6d4f0eb0 Binary files /dev/null and b/src/assets/icons/Copy-icon-24.png differ diff --git a/src/assets/icons/delete-icon-24.png b/src/assets/icons/delete-icon-24.png new file mode 100644 index 00000000..57b6eb6c Binary files /dev/null and b/src/assets/icons/delete-icon-24.png differ diff --git a/src/assets/icons/edit-icon-24.png b/src/assets/icons/edit-icon-24.png new file mode 100644 index 00000000..062d9c09 Binary files /dev/null and b/src/assets/icons/edit-icon-24.png differ diff --git a/src/pages/EditQuestionnaire/Questionnaire.tsx b/src/pages/EditQuestionnaire/Questionnaire.tsx index 29a9d6c4..cb41e131 100644 --- a/src/pages/EditQuestionnaire/Questionnaire.tsx +++ b/src/pages/EditQuestionnaire/Questionnaire.tsx @@ -1,6 +1,9 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; +import { Link } from "react-router-dom"; // Import Link for navigation import ImportModal from "./ImportModal"; import ExportModal from "./ExportModal"; +import axios from "axios"; +import { getAuthToken } from "../../utils/auth"; interface ImportedData { title: string; @@ -37,100 +40,76 @@ const Questionnaire = () => { max_label: "almost never", min_label: "almost always", }, - { - seq: 3.0, - question: "How much did this person offer to do in this project?", - type: "Criterion", - weight: 1, - text_area_size: "50, 30", - max_label: "100%-80%", - min_label: "20%-0%", - }, - { - seq: 4.0, - question: "What fraction of the work assigned to this person did s(he) do?", - type: "Criterion", - weight: 1, - text_area_size: "50, 30", - max_label: "100%-80%", - min_label: "20%-0%", - }, - { - seq: 4.5, - question: "Did this person do assigned work on time?", - type: "Criterion", - weight: 1, - text_area_size: "50, 30", - max_label: "always", - min_label: "never", - }, - { - seq: 5.0, - question: "How much initiative did this person take on this project?", - type: "Criterion", - weight: 1, - text_area_size: "50, 30", - max_label: "a whole lot", - min_label: "total deadbeat", - }, - { - seq: 6.0, - question: "Did this person try to avoid doing any task that was necessary?", - type: "Criterion", - weight: 1, - text_area_size: "50, 30", - max_label: "not at all", - min_label: "absolutely", - }, - { - seq: 7.0, - question: "How many of the useful ideas did this person come up with?", - type: "Criterion", - weight: 1, - text_area_size: "50, 30", - max_label: "100%-80%", - min_label: "20%-0%", - }, - { - seq: 8.0, - question: "What fraction of the coding did this person do?", - type: "Criterion", - weight: 1, - text_area_size: "50, 30", - max_label: "100%-80%", - min_label: "20%-0%", - }, - { - seq: 9.0, - question: "What fraction of the documentation did this person write?", - type: "Criterion", - weight: 1, - text_area_size: "50, 30", - max_label: "100%-80%", - min_label: "20%-0%", - }, - { - seq: 11.0, - question: "How important is this person to the team?", - type: "Criterion", - weight: 1, - text_area_size: "50, 30", - max_label: "indispensable", - min_label: "redundant", - }, ], }; + + const [name, setName] = useState("Default Name"); + const [questionnaireType, setQuestionnaireType] = useState(""); // New state for questionnaire type const [minScore, setMinScore] = useState(0); const [maxScore, setMaxScore] = useState(5); const [isPrivate, setIsPrivate] = useState(false); - const [questionnaireData, setQuestionnaireData] = useState(sample_questionnaire); - const [showImportModal, setShowImportModal] = useState(false); - const [showExportModal, setShowExportModal] = useState(false); + const [questionnaireId, setQuestionnaireId] = useState(null); + + useEffect(() => { + const hash = window.location.hash.substring(1); // Remove the '#' symbol + if (hash) { + const decodedHash = decodeURIComponent(hash); // Decode URL-encoded characters like %20 to spaces + + if (decodedHash.startsWith("_")) { + // If the token starts with '_', set the questionnaire type directly and skip fetching + setQuestionnaireType(decodedHash.substring(1)); // Remove the leading '_' + } else { + const parsedId = Number(decodedHash); // Convert the hash to a number + if (!isNaN(parsedId)) { + setQuestionnaireId(parsedId); // Set the questionnaire ID if it's a valid number + } else { + console.warn("Invalid questionnaire ID in the URL hash:", decodedHash); + } + } + } else { + console.warn("No hash value found in the URL"); + } + }, []); // Runs only once on mount + + // Fetch questionnaire data after the id is set (only if the string does not start with '_') + useEffect(() => { + const fetchQuestionnaireData = async () => { + try { + const response = await axios.get( + "http://localhost:3002/api/v1/questionnaires", + { + headers: { + Authorization: `Bearer ${getAuthToken()}`, // Retrieve the authentication token + }, + } + ); + + const questionnaires = response.data; + const matchedQuestionnaire = questionnaires.find( + (item: any) => item.id === questionnaireId // Match the questionnaire by name + ); + + if (matchedQuestionnaire) { + setName(matchedQuestionnaire.name || ""); // Set name + setQuestionnaireType(matchedQuestionnaire.questionnaire_type || ""); // Set questionnaire type + setMinScore(matchedQuestionnaire.min_question_score || 0); + setMaxScore(matchedQuestionnaire.max_question_score || 5); + setIsPrivate(matchedQuestionnaire.private || false); + setQuestionnaireData(matchedQuestionnaire.data || sample_questionnaire.data); + } else { + console.warn("No matching questionnaire found for the id:", questionnaireId); + } + } catch (error) { + console.error("Error fetching questionnaires:", error); + } + }; + + fetchQuestionnaireData(); + }, [questionnaireId]); // Runs whenever `name` or `questionnaireType` changes - // Function to export questionnaire data const exportQuestionnaire = () => { - const dataToExport = JSON.stringify(questionnaireData); + const dataToExport = JSON.stringify(questionnaireData, null, 2); const blob = new Blob([dataToExport], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); @@ -141,256 +120,180 @@ const Questionnaire = () => { URL.revokeObjectURL(url); }; - // Function to handle imported data const handleFileChange = (importedData: ImportedData) => { setQuestionnaireData(importedData); }; + const handleUpdate = async () => { + if (!questionnaireId) { + alert("Questionnaire ID not found. Please try again."); + return; + } + + const updatedContent = { + name, + questionnaire_type: questionnaireType, // Include questionnaire type in the update + min_question_score: minScore, + max_question_score: maxScore, + private: isPrivate, + data: questionnaireData, + }; + + try { + const token = getAuthToken(); + const response = await axios.put( + `http://localhost:3002/api/v1/questionnaires/${questionnaireId}`, + updatedContent, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + } + ); + + console.log("Update successful:", response.data); + alert("Questionnaire updated successfully!"); + } catch (error) { + if (axios.isAxiosError(error)) { + alert(`Failed to update questionnaire. ${error.response?.data?.message || error.message}`); + } else { + alert(`Failed to update questionnaire. ${String(error)}`); + } + } + }; + + const handleAddToQuestionnaire = async () => { + const updatedContent = { + name, + questionnaire_type: questionnaireType, // Include questionnaire type in the update + min_question_score: minScore, + max_question_score: maxScore, + private: isPrivate, + data: questionnaireData, + instructor_id: 1, + }; + + try { + const token = getAuthToken(); + const response = await axios.post( + `http://localhost:3002/api/v1/questionnaires`, // Use POST for creating a new entry + updatedContent, // Payload for the new entry + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + } + ); + + console.log("Entry creation successful:", response.data); + alert("New questionnaire entry created successfully!"); + + // Update the local state with the server response + setQuestionnaireData(response.data); + } catch (error) { + if (axios.isAxiosError(error)) { + alert(`Failed to create questionnaire. ${error.response?.data?.message || error.message}`); + } else { + alert(`Failed to create questionnaire. ${String(error)}`); + } + } + }; return ( -
-
-

{sample_questionnaire.title}

-
-
- Min item score: +
+
+

Edit Questionnaire

+
+ + Back to Manage Questionnaire Page + +
+
+
+ setName(e.target.value)} // Allow editing of the name + /> +
+
+ + setQuestionnaireType(e.target.value)} // Allow editing of the questionnaire type + /> +
+ +
+ + setMinScore(parseInt(e.target.value, 10))} - // Using parseInt to convert the input value to a number - > + />
-
-
-
- Max item score: + +
+ setMaxScore(parseInt(e.target.value, 10))} - // Using parseInt to convert the input value to a number - > -
-
-
-
- Is this Teammate review private:{' '} - setIsPrivate(!isPrivate)} />
-
-
-
+ +
+ +
+ setIsPrivate(!isPrivate)} + />{" "} + +
+
+ +
-
-
-
- -
-
Seq
-
Question
-
Type
-
Weight
-
Text_area_size
-
Max_label
-
Min_label
-
Action
-
- {sample_questionnaire.data.map((item) => { - return ( -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- ); - })} -
-
-
-
- -
-
-

- more -

-
-
- -
-
-

- question(s) -

-
-
- -
-
-
-
-
- -
-
-
-
- -
-
-
-
- - - {/* Render import and export modals conditionally */} - {showImportModal && ( - setShowImportModal(false)} - onImport={handleFileChange} - /> - )} - {showExportModal && ( - setShowExportModal(false)} - onExport={exportQuestionnaire} - /> - )} -
+ +
); diff --git a/src/pages/Questionnaire/questionnaire.tsx b/src/pages/Questionnaire/questionnaire.tsx index 9f3361b5..c44ce7bb 100644 --- a/src/pages/Questionnaire/questionnaire.tsx +++ b/src/pages/Questionnaire/questionnaire.tsx @@ -1,161 +1,483 @@ -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"; - +import React, { useState, useEffect } from "react"; +import "./Questionnaire.css"; +import { Button } from "react-bootstrap"; +import { BsPlusSquareFill, BsDashSquareFill } from "react-icons/bs"; +import { useNavigate } from "react-router-dom"; +import axios from "axios"; +import { getAuthToken } from "../../utils/auth"; +import DeleteIcon from "../../assets/icons/delete-icon-24.png"; +import CopyIcon from "../../assets/icons/Copy-icon-24.png"; +import EditIcon from "../../assets/icons/edit-icon-24.png"; 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 sample_questionnaire = { + title: "Edit Teammate Review", + data: [ + { + seq: 1.0, + question: "How many times was this person late to meetings?", + type: "Criterion", + weight: 1, + text_area_size: "50, 30", + max_label: "almost never", + min_label: "almost always", + }, + { + seq: 2.0, + question: "How many times did this person not show up?", + type: "Criterion", + weight: 1, + text_area_size: "50, 30", + max_label: "almost never", + min_label: "almost always", + }, + ], + }; + + const [expandedType, setExpandedType] = useState(null); + const [questionnaireItems, setQuestionnaireItems] = useState([]); // Initialize with an empty array + const [newQuestionnaireName, setNewQuestionnaireName] = useState(""); // State for new questionnaire name + const [newQuestionnaireType, setNewQuestionnaireType] = useState(""); // State for new questionnaire type + const [triggerEffect, setTriggerEffect] = useState(false); // State to trigger useEffect + const navigate = useNavigate(); // Initialize navigation - const handleAddButtonClick = () => { - console.log('Add button clicked'); - // Add your logic for adding questionnaire items here + const [questionnaireType, setQuestionnaireType] = useState(""); // New state for questionnaire type + const [minScore, setMinScore] = useState(0); + const [maxScore, setMaxScore] = useState(5); + const [isPrivate, setIsPrivate] = useState(false); + const [questionnaireData, setQuestionnaireData] = useState(sample_questionnaire); + const [questionnaireId, setQuestionnaireId] = useState(null); + const [questionnaireName, setQuestionnaireName] = useState(""); + + // Fetch questionnaire items from the API + const fetchQuestionnaireItems = async () => { + try { + const response = await axios.get("http://localhost:3002/api/v1/questionnaires", { + headers: { + Authorization: `Bearer ${getAuthToken()}`, // Retrieve the authentication token + }, + }); + + if (response.status === 200) { + setQuestionnaireItems(response.data); // Set the fetched items + } else { + console.error("Failed to fetch questionnaire items."); + } + } catch (error) { + console.error("Error fetching questionnaire items:", error); + } }; - type QuestionnaireItem = { - name: string; - creationDate: string; - updatedDate: string; - }; - - const handleItemClick = (index: number) => { - if (expandedItem === index) { - setExpandedItem(null); - } else { - setExpandedItem(index); + useEffect(() => { + fetchQuestionnaireItems(); // Fetch items when the component mounts + }, []); + + const handleCopyitem = async (id: number) => { + try { + // Fetch the questionnaire data for the given name + const response = await axios.get( + "http://localhost:3002/api/v1/questionnaires", + { + headers: { + Authorization: `Bearer ${getAuthToken()}`, // Retrieve the authentication token + }, + } + ); + + const questionnaires = response.data; + const matchedQuestionnaire = questionnaires.find( + (item: any) => item.id === id // Match the questionnaire by name + ); + + if (!matchedQuestionnaire) { + console.warn("No matching questionnaire found for the id:", id); + return; + } + + // Construct updatedContent immediately after fetching data + const updatedContent = { + name: `${matchedQuestionnaire.name}`, // Append "Copy" to the name to avoid conflicts + questionnaire_type: matchedQuestionnaire.questionnaire_type || "", + min_question_score: matchedQuestionnaire.min_question_score || 0, + max_question_score: matchedQuestionnaire.max_question_score || 5, + private: matchedQuestionnaire.private || false, + data: matchedQuestionnaire.data, + instructor_id: 1, + }; + + // Make the POST request to create a new questionnaire + const token = await getAuthToken(); + const postResponse = await axios.post( + `http://localhost:3002/api/v1/questionnaires`, + updatedContent, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + } + ); + + console.log("Entry creation successful:", postResponse.data); + alert("New questionnaire entry created successfully!"); + + // Reload the questionnaire list + fetchQuestionnaireItems(); + } catch (error) { + if (axios.isAxiosError(error)) { + alert(`Failed to create questionnaire. ${error.response?.data?.message || error.message}`); + } else { + alert(`Failed to create questionnaire. ${String(error)}`); + } } }; - const handleDelete = (item: QuestionnaireItem) => { - console.log(`Delete button clicked for item:`, item); - // Add your logic for deleting the item here + useEffect(() => { + if (triggerEffect) { + const createNewQuestionnaire = async () => { + const newQuestionnaire = { + name: newQuestionnaireName, + questionnaire_type: newQuestionnaireType, + instructor_id: 1, + min_question_score: 0, + max_question_score: 5, + private: false, + data: [], + }; + + try { + const token = getAuthToken(); + const response = await axios.post( + `http://localhost:3002/api/v1/questionnaires`, + newQuestionnaire, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + } + ); + + console.log("Creation successful:", response.data); + alert("New questionnaire created successfully!"); + fetchQuestionnaireItems(); // Reload the list + } catch (error) { + if (axios.isAxiosError(error)) { + alert(`Failed to create questionnaire. ${error.response?.data?.message || error.message}`); + } else { + alert(`Failed to create questionnaire. ${String(error)}`); + } + } + }; + + createNewQuestionnaire(); + setTriggerEffect(false); // Reset the trigger + } + }, [triggerEffect, newQuestionnaireName, newQuestionnaireType]); + + const handleNavigateToEditPage = (itemName: string) => { + navigate(`/edit-questionnaire#${encodeURIComponent(itemName)}`); }; - const handleEdit = (item: QuestionnaireItem) => { - console.log(`Edit button clicked for item:`, item); - // Add your logic for editing the item here + const handleDeleteItem = async (itemId: string) => { + try { + const token = getAuthToken(); + await axios.delete(`http://localhost:3002/api/v1/questionnaires/${itemId}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + alert("Item deleted successfully!"); + fetchQuestionnaireItems(); // Reload the list + } catch (error) { + console.error("Error deleting item:", error); + alert("Failed to delete the item."); + } }; - const handleShow = (item: QuestionnaireItem) => { - console.log(`Show button clicked for item:`, item); - // Add your logic for showing the item here + const handleToggleType = (type: string) => { + setExpandedType(expandedType === type ? null : type); }; - const handleSortByName = () => { - if (sortOrder === 'asc') { - setSortOrder('desc'); - } else { - setSortOrder('asc'); + const handleAdd = () => { + if (newQuestionnaireName.trim() === "" || newQuestionnaireType.trim() === "") { + alert("Please enter both a name and a type before adding."); + return; } + setTriggerEffect(true); // Trigger the creation effect }; - const sortedQuestionnaireItems = [...questionnaireItems]; + // Group questionnaires by type + const groupedByType = questionnaireItems.reduce((acc: any, item: any) => { + if (!acc[item.questionnaire_type]) { + acc[item.questionnaire_type] = []; + } + acc[item.questionnaire_type].push(item); + return acc; + }, {}); + + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); - if (sortOrder === 'asc') { - sortedQuestionnaireItems.sort(); - } else if (sortOrder === 'desc') { - sortedQuestionnaireItems.sort().reverse(); - } + // Function to sort alphabetically by name + const sortGroupedByType = (groupedByType: any) => { + const sortedKeys = Object.keys(groupedByType).sort((a, b) => { + if (sortOrder === "asc") { + return a.localeCompare(b); // Ascending order + } else { + return b.localeCompare(a); // Descending order + } + }); - return ( -
-

Questionnaire List

- + // Return a new sorted object based on sorted keys + const sortedGroupedByType: any = {}; + sortedKeys.forEach((key) => { + sortedGroupedByType[key] = groupedByType[key]; + }); -
+ return sortedGroupedByType; + }; - + // Toggle sort order + const toggleSortOrder = () => { + setSortOrder(sortOrder === "asc" ? "desc" : "asc"); + }; + return ( +
+

Manage Content

+
- - + - {sortedQuestionnaireItems.map((item, index) => ( + {Object.keys(sortGroupedByType(groupedByType)).map((type, index) => ( - + - {expandedItem === index && ( - - - -)} + {/* Column 2: Creation Date */} + + {"Creation Date"} + + + {/* Column 3: Updated Date */} + + {"Updated Date"} + + + {/* Column 4: Actions */} + + {"Actions"} + + + + + + )} + {expandedType === type && + groupedByType[type].map((item: any) => ( + + + + ))} ))}
- Name {sortOrder === 'asc' && '↑'} {sortOrder === 'desc' && '↓'} {sortOrder === null && '↑↓'} + + Name{" "} + ActionActions
handleItemClick(index)}>{item.name} handleToggleType(type)}>{type} - - - +
- - - - - - - - - - - - - - - - - - - - - + {expandedType === type && ( + + - -
Name:Creation Date:Updated Date:Actions:
{item.name}{item.creationDate}{item.updatedDate} - - - - - - - - - -
+
+
+ {/* Column 1: Item Name */} + + {"Item Name"} + -
-
+
+ {/* Column 1: Item Name */} + + + {/* Column 2: Created At */} + + + {/* Column 3: Updated At */} + + + {/* Column 4: Action Buttons */} +
+ + + +
+
+