Skip to content
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
10 changes: 0 additions & 10 deletions build.sh

This file was deleted.

4 changes: 0 additions & 4 deletions dev.sh

This file was deleted.

20 changes: 10 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 0 additions & 4 deletions prod.sh

This file was deleted.

1 change: 1 addition & 0 deletions src/@types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type Major = {
}

export type CourseInfo = {
course_id: number,
id: number,
course: number,
course_unit_year: number,
Expand Down
50 changes: 26 additions & 24 deletions src/components/planner/sidebar/sessionController/CsvExport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,7 @@ import { useContext } from 'react'
import CourseContext from '../../../../contexts/CourseContext'
import MultipleOptionsContext from '../../../../contexts/MultipleOptionsContext'
import { AnalyticsTracker, Feature } from '../../../../utils/AnalyticsTracker'

//TODO: utils??
const csvEncode = (text: string | null | undefined) => {
if (!text)
return ''
if (text.includes(','))
return `"${text}"`
return text
}
import { csvEncode } from '../../../../utils/io'

/**
* Sidebar with all the main schedule interactions
Expand All @@ -20,21 +12,31 @@ const CsvExport = () => {
const { pickedCourses } = useContext(CourseContext);
const { multipleOptions } = useContext(MultipleOptionsContext);

const exportCSV = () => {
const header = ['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]
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);

line.push(csvEncode(pickedClass?.name))
})
lines.push(line.join(','))
})
const getOptions = async(): Promise<string[]> => {
const lines = [];

for(const course of pickedCourses) {
const baseInfo = [course.course_id, course.course_unit_year, csvEncode(course.name), course.acronym]

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

return lines;
}
const exportCSV = async () => {
const header = ['ID_Curso', 'Ano', 'Nome', 'Sigla']
multipleOptions.forEach((option) => header.push(option.name));

const lines = await getOptions();
console.log(lines);

const csv = [header.join(','), lines.flat().join('\n')].join('\n')
const blob = new Blob([csv], { type: 'text/csv' })
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-2n 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);
147 changes: 129 additions & 18 deletions src/components/planner/sidebar/sessionController/Export.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,140 @@
import { Button } from '../../../ui/button'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../../../ui/dropdown-menu'
import CsvExport from './CsvExport'
import CsvImport from './CsvImport'
import NitSigExport from './NitSigExport'
import { ArrowDownTrayIcon } from '@heroicons/react/24/solid'
import React, { useContext, useRef } from 'react'
import CourseContext from '../../../../contexts/CourseContext'
import MultipleOptionsContext from '../../../../contexts/MultipleOptionsContext'
import { csvDecode } from '../../../../utils/io'
import { toast } from '../../../ui/use-toast'
import Backend from '../../../../api/backend'
import StorageAPI from '../../../../api/storage'
import { CourseInfo, CourseOption } from '../../../../@types'

/**
* 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 {
// set loadingSchedule to true
const content = csvDecode(await file.text());
const courses = await getSelectedCourses(content);

setCheckboxedCourses(courses);
setPickedCourses(courses);
setCourseOptions(content, courses);
// set loadingSchedule to false
// TODO: THE CLASSES ONLY GET SELECTED AFTER SELECTING ANY RANDOM BUTTON, FIX THIS
} 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',
});
} finally {
// set loadingSchedule to false
}
};

const getSelectedCourses = async (content: string[][]): Promise<CourseInfo[]> => {
if (!Array.isArray(content) || content.length === 0) return [];

const courses: CourseInfo[] = [];

const promises = content.map(async (row) => {
const res = await Backend.getCoursesByMajorId(parseInt(row[0]));
const matchedCourses = res.filter(course => course.acronym === row[3]);
courses.push(...matchedCourses);
});

await Promise.all(promises);

for(const course of courses){
if(typeof(course.ects) === "string")
course.ects = parseFloat(course.ects.replace(",", "."));
}

return courses;
};

const setCourseOptions = async (content: string[][], courses: CourseInfo[]) => {
const scheduleOptions = content.map(row => row.slice(4));
for (let i = 0; i < scheduleOptions[0].length; i++) {
multipleOptions[i].course_options = [];
}

for (let i = 0; i < scheduleOptions.length; i++) {
for(let j = 0; j < 10; j++){
while(courses[i].classes === undefined) await new Promise(resolve => setTimeout(resolve, 100));

const foundClassId = courses[i].classes.find(c => c.name === scheduleOptions[i][j]);
const newCourseOption = createCourseOption(courses[i].id, foundClassId ? foundClassId.id : null);
multipleOptions[j].course_options.push(newCourseOption);
}
}
StorageAPI.setMultipleOptionsStorage(multipleOptions);
setMultipleOptions(multipleOptions);
};


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 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
29 changes: 29 additions & 0 deletions src/utils/io.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const csvEncode = (text: string | null | undefined) => {
if (!text)
return ''
if (text.includes(','))
return `"${text}"`
return text
}

const csvDecode = (text: string | null | undefined) => {
return text
.trim()
.split(/\r?\n/)
.slice(1)
.map(line => {
const fields: string[] = [];
line.replace(/("([^"]|"")*"|[^,]*)(,|$)/g, (_, field) => {
const unquoted = field.replace(/^"|"$/g, '').replace(/""/g, '"');
fields.push(unquoted);
return '';
});
return fields;
});
};


export {
csvEncode,
csvDecode
}