Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/multiple option import #399

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 34 additions & 19 deletions src/components/planner/sidebar/sessionController/CsvExport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 '../../../../utils/io'

//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
Expand All @@ -20,23 +13,45 @@ const CsvExport = () => {
const { pickedCourses } = useContext(CourseContext);
const { multipleOptions } = useContext(MultipleOptionsContext);

const GET_NAMES = true;
const GET_IDS = false;

RuiSoares333 marked this conversation as resolved.
Show resolved Hide resolved
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 = ['Ano', 'Nome', 'Sigla']
multipleOptions.forEach((option) => header.push(option.name))
const lines = []
header.push(pickedCourses.length.toString())

pickedCourses.forEach(course => {
const line = [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')
Expand Down
23 changes: 23 additions & 0 deletions src/components/planner/sidebar/sessionController/CsvImport.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';

const CsvImport = ({handleClick}) => {
return (
<div>
<button
onClick={handleClick}
className="group flex w-full items-center gap-2 dark:text-white rounded-md p-1 text-gray text-sm disabled:cursor-not-allowed disabled:opacity-50"
>
<ArrowDownOnSquareIcon className="h-5 w-5 text-secondary hover:brightness-200" />
<span className="pl-1">Importar Opções (CSV)</span>
</button>
</div>
);
};

CsvImport.propTypes = {
handleClick: PropTypes.func.isRequired,
};

export default React.memo(CsvImport);
144 changes: 129 additions & 15 deletions src/components/planner/sidebar/sessionController/Export.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,141 @@ 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, { useContext, useRef } from 'react'
import { csvDecode } from '../../../../utils/io'
import CourseContext from '../../../../contexts/CourseContext'
import MultipleOptionsContext from '../../../../contexts/MultipleOptionsContext'
import api from '../../../../api/backend'
import { Option, CourseInfo, CourseOption } from '../../../../@types'
import { toast } from '../../../ui/use-toast'

/**
* Sidebar with all the main schedule interactions
*/
const Export = () => {

const fileInputRef = useRef(null);
const { setPickedCourses, setCheckboxedCourses } = useContext(CourseContext);
const { multipleOptions, setMultipleOptions } = useContext(MultipleOptionsContext);

const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files[0];
if (!file) {
throw new Error('No file selected');
}

try {
const content = csvDecode(await file.text());
const courses = await getSelectedCourses(content);

setCheckboxedCourses(courses);
setPickedCourses(courses);
setCourseOptions(content);
} catch (error) {
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<CourseInfo[]> => {
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);
if (matching_course) {
course.ects = matching_course.ects;
}
});

return selected_courses;
};

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<CourseOption>) => {
return {
id: option.id,
icon: option.icon,
name: option.name,
course_options: new_course_options
}
}

const handleClick = () => {
fileInputRef.current.click();
};

const inputComponent = (
<input
type="file"
accept=".csv"
ref={fileInputRef}
className="hidden"
onChange={async (e) => {
await handleFileChange(e)
}}
/>
)

const menuItems = [
{component: <CsvExport/>},
{component: <CsvImport handleClick={handleClick}/>},
{component: <NitSigExport/>}
]


return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="icon" className="bg-primary">
<ArrowDownTrayIcon className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<CsvExport />
</DropdownMenuItem>
<DropdownMenuItem>
<NitSigExport />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<>
{inputComponent}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="icon" className="bg-primary">
<ArrowDownTrayIcon className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{menuItems.map((item, index) => (
<DropdownMenuItem key={index}>
{item.component}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</>
)
}

Expand Down
30 changes: 30 additions & 0 deletions src/utils/io.ts
Original file line number Diff line number Diff line change
@@ -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
}