From 222e98f01771e1111fc0a570200edc574ed555b1 Mon Sep 17 00:00:00 2001 From: Rui Soares Date: Tue, 19 Nov 2024 18:14:55 +0000 Subject: [PATCH 1/7] parsing csv into bidimensional array of strings --- .../sidebar/sessionController/CsvExport.tsx | 9 +-- .../sidebar/sessionController/CsvImport.tsx | 23 ++++++ .../sidebar/sessionController/Export.tsx | 79 +++++++++++++++---- .../sidebar/sessionController/IOUtils.tsx | 28 +++++++ src/pages/TimeTableSelector.tsx | 2 +- 5 files changed, 117 insertions(+), 24 deletions(-) create mode 100644 src/components/planner/sidebar/sessionController/CsvImport.tsx create mode 100644 src/components/planner/sidebar/sessionController/IOUtils.tsx diff --git a/src/components/planner/sidebar/sessionController/CsvExport.tsx b/src/components/planner/sidebar/sessionController/CsvExport.tsx index d0b63cea..61086c66 100644 --- a/src/components/planner/sidebar/sessionController/CsvExport.tsx +++ b/src/components/planner/sidebar/sessionController/CsvExport.tsx @@ -3,15 +3,8 @@ import { useContext } from 'react' import CourseContext from '../../../../contexts/CourseContext' import MultipleOptionsContext from '../../../../contexts/MultipleOptionsContext' import { AnalyticsTracker, Feature } from '../../../../utils/AnalyticsTracker' +import { csvEncode } from './IOUtils' -//TODO: utils?? -const csvEncode = (text: string | null | undefined) => { - if (!text) - return '' - if (text.includes(',')) - return `"${text}"` - return text -} /** * Sidebar with all the main schedule interactions diff --git a/src/components/planner/sidebar/sessionController/CsvImport.tsx b/src/components/planner/sidebar/sessionController/CsvImport.tsx new file mode 100644 index 00000000..8a0b63be --- /dev/null +++ b/src/components/planner/sidebar/sessionController/CsvImport.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; + +const CsvImport = ({handleClick}) => { + return ( +
+ +
+ ); +}; + +CsvImport.propTypes = { + handleClick: PropTypes.func.isRequired, +}; + +export default React.memo(CsvImport); \ No newline at end of file diff --git a/src/components/planner/sidebar/sessionController/Export.tsx b/src/components/planner/sidebar/sessionController/Export.tsx index 8ee183fc..28ac1c51 100644 --- a/src/components/planner/sidebar/sessionController/Export.tsx +++ b/src/components/planner/sidebar/sessionController/Export.tsx @@ -3,27 +3,76 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge import CsvExport from './CsvExport' import NitSigExport from './NitSigExport' import { ArrowDownTrayIcon } from '@heroicons/react/24/solid' +import CsvImport from './CsvImport' +import React, { useRef } from 'react' +import { csvDecode } from './IOUtils' /** * Sidebar with all the main schedule interactions */ const Export = () => { + + const fileInputRef = useRef(null); + + const handleFileChange = async (event: React.ChangeEvent) => { + const file = event.target.files[0]; + if (!file) { + // TODO - handle error + return; + } + + try { + const content = csvDecode(await file.text()); + // TODO - convert year/name/acronym to picked courses + // TODO - convert options to multiple options + console.log(content) + } catch (error) { + console.error("Error reading or parsing CSV file:", error); + alert("Failed to import CSV. Please check the file format."); + } + }; + + const handleClick = () => { + fileInputRef.current.click(); + }; + + const inputComponent = ( + { + await handleFileChange(e) + }} + /> + ) + + const menuItems = [ + {component: }, + {component: }, + {component: } + ] + + return ( - - - - - - - - - - - - - + <> + {inputComponent} + + + + + + {menuItems.map((item, index) => ( + + {item.component} + + ))} + + + ) } diff --git a/src/components/planner/sidebar/sessionController/IOUtils.tsx b/src/components/planner/sidebar/sessionController/IOUtils.tsx new file mode 100644 index 00000000..38efbb48 --- /dev/null +++ b/src/components/planner/sidebar/sessionController/IOUtils.tsx @@ -0,0 +1,28 @@ +const csvEncode = (text: string | null | undefined) => { + if (!text) + return '' + if (text.includes(',')) + return `'${text}'` + return text +} + +const csvDecode = (text: string | null | undefined) => { + return text.split('\n').map(line => { + const quotesIndex = line.indexOf('\''); + if (quotesIndex !== -1) { + const start = quotesIndex; + const end = line.lastIndexOf('\''); + const slicedContent = line.slice(0, start) + line.slice(end + 1); + const parts = slicedContent.split(','); + parts[1] = line.slice(start + 1, end); + return parts; + } + return line.split(','); + }); +}; + + +export { + csvEncode, + csvDecode +} \ No newline at end of file diff --git a/src/pages/TimeTableSelector.tsx b/src/pages/TimeTableSelector.tsx index 9eb6f343..da8ed1b9 100644 --- a/src/pages/TimeTableSelector.tsx +++ b/src/pages/TimeTableSelector.tsx @@ -13,7 +13,7 @@ const TimeTableSelectorPage = () => { const [checkboxedCourses, setCheckboxedCourses] = useState(StorageAPI.getPickedCoursesStorage()); const [ucsModalOpen, setUcsModalOpen] = useState(false); - //TODO: Looks suspicious + //TODO: Looks suspicious ඞ const [choosingNewCourse, setChoosingNewCourse] = useState(false); useEffect(() => { document.getElementById('layout').scrollIntoView() From f36b1bfb85772e2ae7c3f5475ddba9060ea421bc Mon Sep 17 00:00:00 2001 From: Rui Soares Date: Thu, 16 Jan 2025 00:27:07 +0000 Subject: [PATCH 2/7] successfully set the selected classes after import --- .../sidebar/sessionController/CsvExport.tsx | 4 +- .../sidebar/sessionController/Export.tsx | 37 +++++++++++++++---- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/components/planner/sidebar/sessionController/CsvExport.tsx b/src/components/planner/sidebar/sessionController/CsvExport.tsx index 61086c66..0cc1f0b0 100644 --- a/src/components/planner/sidebar/sessionController/CsvExport.tsx +++ b/src/components/planner/sidebar/sessionController/CsvExport.tsx @@ -14,12 +14,12 @@ const CsvExport = () => { const { multipleOptions } = useContext(MultipleOptionsContext); const exportCSV = () => { - const header = ['Ano', 'Nome', 'Sigla'] + const header = ['ID', 'Ano', 'Nome', 'Sigla'] multipleOptions.forEach((option) => header.push(option.name)) const lines = [] pickedCourses.forEach(course => { - const line = [course.course_unit_year, csvEncode(course.name), course.acronym] + const line = [course.id, course.course_unit_year, csvEncode(course.name), course.acronym] multipleOptions.forEach(option => { const courseOption = option.course_options.find(courseOption => courseOption.course_id === course.id) const pickedClass = course.classes.find(c => c.id === courseOption?.picked_class_id); diff --git a/src/components/planner/sidebar/sessionController/Export.tsx b/src/components/planner/sidebar/sessionController/Export.tsx index 28ac1c51..fefb079f 100644 --- a/src/components/planner/sidebar/sessionController/Export.tsx +++ b/src/components/planner/sidebar/sessionController/Export.tsx @@ -4,8 +4,14 @@ import CsvExport from './CsvExport' import NitSigExport from './NitSigExport' import { ArrowDownTrayIcon } from '@heroicons/react/24/solid' import CsvImport from './CsvImport' -import React, { useRef } from 'react' +import React, { useContext, useRef } from 'react' import { csvDecode } from './IOUtils' +import CourseContext from '../../../../contexts/CourseContext' +import MultipleOptionsContext from '../../../../contexts/MultipleOptionsContext' +import api from '../../../../api/backend' +import { CourseInfo } from '../../../../@types' +import StorageAPI from '../../../../api/storage' +import { toast } from '../../../ui/use-toast' /** * Sidebar with all the main schedule interactions @@ -13,25 +19,40 @@ import { csvDecode } from './IOUtils' const Export = () => { const fileInputRef = useRef(null); + const { setPickedCourses, setCheckboxedCourses } = useContext(CourseContext); const handleFileChange = async (event: React.ChangeEvent) => { const file = event.target.files[0]; if (!file) { - // TODO - handle error - return; + throw new Error('No file selected'); } try { const content = csvDecode(await file.text()); - // TODO - convert year/name/acronym to picked courses - // TODO - convert options to multiple options - console.log(content) + const courses = await getSelectedCourses(content); + + StorageAPI.setPickedCoursesStorage(courses); + setCheckboxedCourses(courses); + setPickedCourses(courses); + } catch (error) { - console.error("Error reading or parsing CSV file:", error); - alert("Failed to import CSV. Please check the file format."); + toast({ + title: 'Não foi possível importar os horários!', + description: 'Ocorreu um erro ao ler ou interpretar o ficheiro importado: ' + error, + position: 'top-right', + }); } }; + const getSelectedCourses = async (content: any): Promise => { + if (!Array.isArray(content) || content.length === 0) return []; + + const results = await Promise.all(content.slice(1).map(row => api.getCourseUnit(row[0]))); + + return results; + }; + + const handleClick = () => { fileInputRef.current.click(); }; From 595e69a7e6762751117fcff4b9cccb94ecc3439c Mon Sep 17 00:00:00 2001 From: Rui Soares Date: Sun, 2 Feb 2025 05:27:40 +0000 Subject: [PATCH 3/7] importing schedule working as intended --- .../sidebar/sessionController/CsvExport.tsx | 50 ++++++++++----- .../sidebar/sessionController/Export.tsx | 61 ++++++++++++++++--- .../sidebar/sessionController/IOUtils.tsx | 28 --------- src/utils/io.ts | 30 +++++++++ 4 files changed, 120 insertions(+), 49 deletions(-) delete mode 100644 src/components/planner/sidebar/sessionController/IOUtils.tsx create mode 100644 src/utils/io.ts diff --git a/src/components/planner/sidebar/sessionController/CsvExport.tsx b/src/components/planner/sidebar/sessionController/CsvExport.tsx index 0cc1f0b0..b4da9fb5 100644 --- a/src/components/planner/sidebar/sessionController/CsvExport.tsx +++ b/src/components/planner/sidebar/sessionController/CsvExport.tsx @@ -1,9 +1,9 @@ import { ArrowUpOnSquareIcon } from '@heroicons/react/24/outline' -import { useContext } from 'react' +import { useContext, useEffect } from 'react' import CourseContext from '../../../../contexts/CourseContext' import MultipleOptionsContext from '../../../../contexts/MultipleOptionsContext' import { AnalyticsTracker, Feature } from '../../../../utils/AnalyticsTracker' -import { csvEncode } from './IOUtils' +import { csvEncode } from '../../../../utils/io' /** @@ -13,23 +13,45 @@ const CsvExport = () => { const { pickedCourses } = useContext(CourseContext); const { multipleOptions } = useContext(MultipleOptionsContext); + const GET_NAMES = true; + const GET_IDS = false; + + const getOptions = (getByName: boolean): string[] => { + return pickedCourses.map(course => { + + const line = getByName + ? [course.course_unit_year, csvEncode(course.name), course.acronym] + : [course.id]; + + multipleOptions.forEach(option => { + const courseOption = option.course_options.find(co => co.course_id === course.id); + const pickedClass = courseOption + ? course.classes.find(c => c.id === courseOption.picked_class_id) + : undefined; + + const value = getByName ? pickedClass?.name : pickedClass?.id?.toString(); + line.push(csvEncode(value || '')); + }); + + return line.join(','); + }); + }; + const exportCSV = () => { - const header = ['ID', 'Ano', 'Nome', 'Sigla'] + const header = ['Ano', 'Nome', 'Sigla'] multipleOptions.forEach((option) => header.push(option.name)) - const lines = [] + header.push(pickedCourses.length.toString()) - pickedCourses.forEach(course => { - const line = [course.id, course.course_unit_year, csvEncode(course.name), course.acronym] - multipleOptions.forEach(option => { - const courseOption = option.course_options.find(courseOption => courseOption.course_id === course.id) - const pickedClass = course.classes.find(c => c.id === courseOption?.picked_class_id); + const lines = getOptions(GET_NAMES); + + lines.push("////----////----////----////----////----////----////") + + const header_ids = ['UC_ID'] + multipleOptions.forEach((option) => header_ids.push(option.name + "_ID")) - line.push(csvEncode(pickedClass?.name)) - }) - lines.push(line.join(',')) - }) + const lines_id = getOptions(GET_IDS); - const csv = [header.join(','), lines.flat().join('\n')].join('\n') + const csv = [header.join(','), lines.flat().join('\n'), header_ids.join(','), lines_id.flat().join('\n')].join('\n') const blob = new Blob([csv], { type: 'text/csv' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') diff --git a/src/components/planner/sidebar/sessionController/Export.tsx b/src/components/planner/sidebar/sessionController/Export.tsx index fefb079f..4d00fba3 100644 --- a/src/components/planner/sidebar/sessionController/Export.tsx +++ b/src/components/planner/sidebar/sessionController/Export.tsx @@ -5,12 +5,11 @@ import NitSigExport from './NitSigExport' import { ArrowDownTrayIcon } from '@heroicons/react/24/solid' import CsvImport from './CsvImport' import React, { useContext, useRef } from 'react' -import { csvDecode } from './IOUtils' +import { csvDecode } from '../../../../utils/io' import CourseContext from '../../../../contexts/CourseContext' import MultipleOptionsContext from '../../../../contexts/MultipleOptionsContext' import api from '../../../../api/backend' import { CourseInfo } from '../../../../@types' -import StorageAPI from '../../../../api/storage' import { toast } from '../../../ui/use-toast' /** @@ -20,6 +19,7 @@ const Export = () => { const fileInputRef = useRef(null); const { setPickedCourses, setCheckboxedCourses } = useContext(CourseContext); + const { multipleOptions, setMultipleOptions } = useContext(MultipleOptionsContext); const handleFileChange = async (event: React.ChangeEvent) => { const file = event.target.files[0]; @@ -31,10 +31,9 @@ const Export = () => { const content = csvDecode(await file.text()); const courses = await getSelectedCourses(content); - StorageAPI.setPickedCoursesStorage(courses); setCheckboxedCourses(courses); setPickedCourses(courses); - + setCourseOptions(content); } catch (error) { toast({ title: 'Não foi possível importar os horários!', @@ -46,12 +45,60 @@ const Export = () => { const getSelectedCourses = async (content: any): Promise => { if (!Array.isArray(content) || content.length === 0) return []; + + try { + // Fetch all courses in parallel + const selected_courses = await Promise.all(content.map(row => api.getCourseUnit(row[0]))); + + // Fetch all majors in parallel + const majorsPromises = selected_courses.map(course => api.getCoursesByMajorId(course.course)); + const majorsResults = await Promise.all(majorsPromises); + + // Map the ECTS values to the corresponding courses + selected_courses.forEach((course, index) => { + const full_courses = majorsResults[index]; + const matching_course = full_courses.find(indiv_course => indiv_course.course_unit_id === course.id); + if (matching_course) { + course.ects = matching_course.ects; + } + }); + + return selected_courses; + } catch (error) { + throw error; + } + }; - const results = await Promise.all(content.slice(1).map(row => api.getCourseUnit(row[0]))); - - return results; + const setCourseOptions = (courses: number[][]) => { + const transposedCourses = courses[0].map((_, colIndex) => courses.map(row => row[colIndex])); + + const newOptions = transposedCourses.slice(1, 11).map((column, i) => + createOption(multipleOptions[i], column.map((value, j) => + createCourseOption(transposedCourses[0][j], value) + )) + ); + + setMultipleOptions(newOptions); }; + const createCourseOption = (course_id: number, picked_class_id: number): CourseOption => { + return { + course_id: course_id, + picked_class_id: Number.isNaN(picked_class_id) ? null : picked_class_id, + locked: false, + filteredTeachers: null, + hide: [] + } + } + + const createOption = (option : Option, new_course_options : Array) => { + return { + id: option.id, + icon: option.icon, + name: option.name, + course_options: new_course_options + } + } const handleClick = () => { fileInputRef.current.click(); diff --git a/src/components/planner/sidebar/sessionController/IOUtils.tsx b/src/components/planner/sidebar/sessionController/IOUtils.tsx deleted file mode 100644 index 38efbb48..00000000 --- a/src/components/planner/sidebar/sessionController/IOUtils.tsx +++ /dev/null @@ -1,28 +0,0 @@ -const csvEncode = (text: string | null | undefined) => { - if (!text) - return '' - if (text.includes(',')) - return `'${text}'` - return text -} - -const csvDecode = (text: string | null | undefined) => { - return text.split('\n').map(line => { - const quotesIndex = line.indexOf('\''); - if (quotesIndex !== -1) { - const start = quotesIndex; - const end = line.lastIndexOf('\''); - const slicedContent = line.slice(0, start) + line.slice(end + 1); - const parts = slicedContent.split(','); - parts[1] = line.slice(start + 1, end); - return parts; - } - return line.split(','); - }); -}; - - -export { - csvEncode, - csvDecode -} \ No newline at end of file diff --git a/src/utils/io.ts b/src/utils/io.ts new file mode 100644 index 00000000..230850b6 --- /dev/null +++ b/src/utils/io.ts @@ -0,0 +1,30 @@ +const csvEncode = (text: string | null | undefined) => { + if (!text) + return '' + if (text.includes(',')) + return `'${text}'` + return text + } + + const csvDecode = (text: string | null | undefined) => { + const lines = text.split('\n'); + + const first_line = lines[0].split(','); + const nr_of_courses = first_line[first_line.length - 1]; + + const id_start = parseInt(nr_of_courses) + 3 + + const id_lines = lines.slice(id_start, lines.length); + + return id_lines.map(line =>{ + return line.split(',').map(option_number => { + return parseInt(option_number); + }); + }); + }; + + + export { + csvEncode, + csvDecode + } \ No newline at end of file From 0d413113eede023069ef154f392599a4e7826162 Mon Sep 17 00:00:00 2001 From: Rui Soares Date: Mon, 3 Feb 2025 14:26:04 +0000 Subject: [PATCH 4/7] linted code --- .../sidebar/sessionController/CsvExport.tsx | 2 +- .../sidebar/sessionController/Export.tsx | 39 +++++++++---------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/components/planner/sidebar/sessionController/CsvExport.tsx b/src/components/planner/sidebar/sessionController/CsvExport.tsx index b4da9fb5..0b2c3ba3 100644 --- a/src/components/planner/sidebar/sessionController/CsvExport.tsx +++ b/src/components/planner/sidebar/sessionController/CsvExport.tsx @@ -1,5 +1,5 @@ import { ArrowUpOnSquareIcon } from '@heroicons/react/24/outline' -import { useContext, useEffect } from 'react' +import { useContext } from 'react' import CourseContext from '../../../../contexts/CourseContext' import MultipleOptionsContext from '../../../../contexts/MultipleOptionsContext' import { AnalyticsTracker, Feature } from '../../../../utils/AnalyticsTracker' diff --git a/src/components/planner/sidebar/sessionController/Export.tsx b/src/components/planner/sidebar/sessionController/Export.tsx index 4d00fba3..6e41fda8 100644 --- a/src/components/planner/sidebar/sessionController/Export.tsx +++ b/src/components/planner/sidebar/sessionController/Export.tsx @@ -46,27 +46,24 @@ const Export = () => { const getSelectedCourses = async (content: any): Promise => { if (!Array.isArray(content) || content.length === 0) return []; - try { - // Fetch all courses in parallel - const selected_courses = await Promise.all(content.map(row => api.getCourseUnit(row[0]))); - - // Fetch all majors in parallel - const majorsPromises = selected_courses.map(course => api.getCoursesByMajorId(course.course)); - const majorsResults = await Promise.all(majorsPromises); - - // Map the ECTS values to the corresponding courses - selected_courses.forEach((course, index) => { - const full_courses = majorsResults[index]; - const matching_course = full_courses.find(indiv_course => indiv_course.course_unit_id === course.id); - if (matching_course) { - course.ects = matching_course.ects; - } - }); - - return selected_courses; - } catch (error) { - throw error; - } + + // Fetch all courses in parallel + const selected_courses = await Promise.all(content.map(row => api.getCourseUnit(row[0]))); + + // Fetch all majors in parallel + const majorsPromises = selected_courses.map(course => api.getCoursesByMajorId(course.course)); + const majorsResults = await Promise.all(majorsPromises); + + // Map the ECTS values to the corresponding courses + selected_courses.forEach((course, index) => { + const full_courses = majorsResults[index]; + const matching_course = full_courses.find(indiv_course => indiv_course.course_unit_id === course.id); + if (matching_course) { + course.ects = matching_course.ects; + } + }); + + return selected_courses; }; const setCourseOptions = (courses: number[][]) => { From 7fecf04609c165561df0fb5a15409ce91dd7f6bf Mon Sep 17 00:00:00 2001 From: Rui Soares Date: Mon, 3 Feb 2025 14:30:06 +0000 Subject: [PATCH 5/7] lint for unimported types --- src/components/planner/sidebar/sessionController/Export.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/planner/sidebar/sessionController/Export.tsx b/src/components/planner/sidebar/sessionController/Export.tsx index 6e41fda8..c88e4bfa 100644 --- a/src/components/planner/sidebar/sessionController/Export.tsx +++ b/src/components/planner/sidebar/sessionController/Export.tsx @@ -9,7 +9,7 @@ import { csvDecode } from '../../../../utils/io' import CourseContext from '../../../../contexts/CourseContext' import MultipleOptionsContext from '../../../../contexts/MultipleOptionsContext' import api from '../../../../api/backend' -import { CourseInfo } from '../../../../@types' +import { Option, CourseInfo, CourseOption } from '../../../../@types' import { toast } from '../../../ui/use-toast' /** From 5f1e301866e384ab6243a13804c829a46bcb1160 Mon Sep 17 00:00:00 2001 From: Rui Soares Date: Thu, 13 Feb 2025 16:12:08 +0000 Subject: [PATCH 6/7] enum instead of boolean cleanup --- .../sidebar/sessionController/CsvExport.tsx | 36 +++++++++---------- .../sidebar/sessionController/Export.tsx | 4 --- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/src/components/planner/sidebar/sessionController/CsvExport.tsx b/src/components/planner/sidebar/sessionController/CsvExport.tsx index 0b2c3ba3..53aaf1cc 100644 --- a/src/components/planner/sidebar/sessionController/CsvExport.tsx +++ b/src/components/planner/sidebar/sessionController/CsvExport.tsx @@ -13,43 +13,41 @@ const CsvExport = () => { const { pickedCourses } = useContext(CourseContext); const { multipleOptions } = useContext(MultipleOptionsContext); - const GET_NAMES = true; - const GET_IDS = false; + enum GetOptionsBy {NAME, ID} - const getOptions = (getByName: boolean): string[] => { - return pickedCourses.map(course => { - - const line = getByName - ? [course.course_unit_year, csvEncode(course.name), course.acronym] - : [course.id]; + const getOptions = (getByName: GetOptionsBy): string[] => + pickedCourses.map(course => { + const baseInfo = getByName === GetOptionsBy.NAME ? + [course.course_unit_year, csvEncode(course.name), course.acronym] : + [course.id]; - multipleOptions.forEach(option => { + const classValues = multipleOptions.map(option => { const courseOption = option.course_options.find(co => co.course_id === course.id); - const pickedClass = courseOption - ? course.classes.find(c => c.id === courseOption.picked_class_id) - : undefined; + const pickedClass = courseOption ? + course.classes.find(c => c.id === courseOption.picked_class_id) : + undefined; - const value = getByName ? pickedClass?.name : pickedClass?.id?.toString(); - line.push(csvEncode(value || '')); + return csvEncode(getByName === GetOptionsBy.NAME ? pickedClass?.name : pickedClass?.id?.toString() || ''); }); - return line.join(','); - }); - }; + return [...baseInfo, ...classValues].join(','); + } + ); + const exportCSV = () => { const header = ['Ano', 'Nome', 'Sigla'] multipleOptions.forEach((option) => header.push(option.name)) header.push(pickedCourses.length.toString()) - const lines = getOptions(GET_NAMES); + const lines = getOptions(GetOptionsBy.NAME); lines.push("////----////----////----////----////----////----////") const header_ids = ['UC_ID'] multipleOptions.forEach((option) => header_ids.push(option.name + "_ID")) - const lines_id = getOptions(GET_IDS); + const lines_id = getOptions(GetOptionsBy.ID); const csv = [header.join(','), lines.flat().join('\n'), header_ids.join(','), lines_id.flat().join('\n')].join('\n') const blob = new Blob([csv], { type: 'text/csv' }) diff --git a/src/components/planner/sidebar/sessionController/Export.tsx b/src/components/planner/sidebar/sessionController/Export.tsx index c88e4bfa..d12a933a 100644 --- a/src/components/planner/sidebar/sessionController/Export.tsx +++ b/src/components/planner/sidebar/sessionController/Export.tsx @@ -46,15 +46,11 @@ const Export = () => { const getSelectedCourses = async (content: any): Promise => { if (!Array.isArray(content) || content.length === 0) return []; - - // Fetch all courses in parallel const selected_courses = await Promise.all(content.map(row => api.getCourseUnit(row[0]))); - // Fetch all majors in parallel const majorsPromises = selected_courses.map(course => api.getCoursesByMajorId(course.course)); const majorsResults = await Promise.all(majorsPromises); - // Map the ECTS values to the corresponding courses selected_courses.forEach((course, index) => { const full_courses = majorsResults[index]; const matching_course = full_courses.find(indiv_course => indiv_course.course_unit_id === course.id); From 82bb9559e8a8a39347bd475c87745e2046b11854 Mon Sep 17 00:00:00 2001 From: Rui Soares Date: Thu, 27 Feb 2025 20:41:50 +0000 Subject: [PATCH 7/7] simplified import and export --- .../sidebar/sessionController/CsvExport.tsx | 43 +++++------- .../sidebar/sessionController/CsvImport.tsx | 2 +- .../sidebar/sessionController/Export.tsx | 67 +++++++++---------- src/utils/io.ts | 15 +---- 4 files changed, 53 insertions(+), 74 deletions(-) diff --git a/src/components/planner/sidebar/sessionController/CsvExport.tsx b/src/components/planner/sidebar/sessionController/CsvExport.tsx index 53aaf1cc..e271c0ac 100644 --- a/src/components/planner/sidebar/sessionController/CsvExport.tsx +++ b/src/components/planner/sidebar/sessionController/CsvExport.tsx @@ -4,6 +4,7 @@ import CourseContext from '../../../../contexts/CourseContext' import MultipleOptionsContext from '../../../../contexts/MultipleOptionsContext' import { AnalyticsTracker, Feature } from '../../../../utils/AnalyticsTracker' import { csvEncode } from '../../../../utils/io' +import api from '../../../../api/backend' /** @@ -13,43 +14,33 @@ const CsvExport = () => { const { pickedCourses } = useContext(CourseContext); const { multipleOptions } = useContext(MultipleOptionsContext); - enum GetOptionsBy {NAME, ID} + const getOptions = async (): Promise => { + const lines = []; + + for(const course of pickedCourses) { + const baseInfo = [course.course_id, course.course_unit_year, csvEncode(course.name), course.acronym] - const getOptions = (getByName: GetOptionsBy): string[] => - pickedCourses.map(course => { - const baseInfo = getByName === GetOptionsBy.NAME ? - [course.course_unit_year, csvEncode(course.name), course.acronym] : - [course.id]; - const classValues = multipleOptions.map(option => { const courseOption = option.course_options.find(co => co.course_id === course.id); const pickedClass = courseOption ? course.classes.find(c => c.id === courseOption.picked_class_id) : undefined; - - return csvEncode(getByName === GetOptionsBy.NAME ? pickedClass?.name : pickedClass?.id?.toString() || ''); + + return csvEncode(pickedClass?.name); }); - - return [...baseInfo, ...classValues].join(','); + lines.push([...baseInfo, ...classValues].join(',')); } - ); - - const exportCSV = () => { - const header = ['Ano', 'Nome', 'Sigla'] - multipleOptions.forEach((option) => header.push(option.name)) - header.push(pickedCourses.length.toString()) - - const lines = getOptions(GetOptionsBy.NAME); - - lines.push("////----////----////----////----////----////----////") - - const header_ids = ['UC_ID'] - multipleOptions.forEach((option) => header_ids.push(option.name + "_ID")) + return lines; + } + + const exportCSV = async () => { + const header = ['ID_Curso', 'Ano', 'Nome', 'Sigla'] + multipleOptions.forEach((option) => header.push(option.name)); - const lines_id = getOptions(GetOptionsBy.ID); + const lines = await getOptions(); - const csv = [header.join(','), lines.flat().join('\n'), header_ids.join(','), lines_id.flat().join('\n')].join('\n') + const csv = [header.join(','), lines.flat().join('\n')].join('\n') const blob = new Blob([csv], { type: 'text/csv' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') diff --git a/src/components/planner/sidebar/sessionController/CsvImport.tsx b/src/components/planner/sidebar/sessionController/CsvImport.tsx index 8a0b63be..a6598b0b 100644 --- a/src/components/planner/sidebar/sessionController/CsvImport.tsx +++ b/src/components/planner/sidebar/sessionController/CsvImport.tsx @@ -7,7 +7,7 @@ const CsvImport = ({handleClick}) => {