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 all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 32 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,43 @@ const CsvExport = () => {
const { pickedCourses } = useContext(CourseContext);
const { multipleOptions } = useContext(MultipleOptionsContext);

enum GetOptionsBy {NAME, 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];

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 [...baseInfo, ...classValues].join(',');
}
);


const exportCSV = () => {
const header = ['Ano', 'Nome', 'Sigla']
multipleOptions.forEach((option) => header.push(option.name))
const lines = []
header.push(pickedCourses.length.toString())

const lines = getOptions(GetOptionsBy.NAME);

lines.push("////----////----////----////----////----////----////")

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 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(GetOptionsBy.ID);

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);
140 changes: 125 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,137 @@ 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 [];

const selected_courses = await Promise.all(content.map(row => api.getCourseUnit(row[0])));

const majorsPromises = selected_courses.map(course => api.getCoursesByMajorId(course.course));
const majorsResults = await Promise.all(majorsPromises);

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
}