Skip to content

Commit

Permalink
Matching reupload (#121)
Browse files Browse the repository at this point in the history
* adding course phase participation table to the shared library

* adding support to also download scores or other meta data

* supporting reupload

* finish matching phase

* aligning button
  • Loading branch information
niclasheun authored Jan 29, 2025
1 parent 484499e commit cbaf602
Show file tree
Hide file tree
Showing 26 changed files with 1,129 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,11 @@ export const ApplicationsAssessment = (): JSX.Element => {
(participation) => participation.id === selectedApplicationID,
)

const tableColumns = columns(viewApplication, deleteApplication, additionalScores ?? [])

const table = useReactTable({
data: participations ?? [],
columns: columns(viewApplication, deleteApplication, additionalScores ?? []),
columns: tableColumns,
getCoreRowModel: getCoreRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
Expand Down Expand Up @@ -162,7 +164,7 @@ export const ApplicationsAssessment = (): JSX.Element => {
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className='h-24 text-center'>
<TableCell colSpan={tableColumns.length} className='h-24 text-center'>
No results.
</TableCell>
</TableRow>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ColumnDef } from '@tanstack/react-table'
import translations from '@/lib/translations.json'
import { SortableHeader } from './SortableHeader'
import { SortableHeader } from '@/components/table/SortableHeader'
import { getStatusBadge } from '../../utils/getStatusBadge'
import { getGenderString } from '@tumaet/prompt-shared-state'
import { Checkbox } from '@/components/ui/checkbox'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { MoreHorizontal, Trash2, FileDown, CheckCircle, XCircle } from 'lucide-react'
import { ActionDialog } from '../../GroupActionDialog/GroupActionDialog'
import { ActionDialog } from '@/components/table/GroupActionDialog'
import { RowModel } from '@tanstack/react-table'
import { ApplicationParticipation } from '../../../../../interfaces/applicationParticipation'
import { useApplicationStatusUpdate } from '../../../hooks/useApplicationStatusUpdate'
Expand Down
1 change: 1 addition & 0 deletions clients/matching_component/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
},
"packageManager": "[email protected]",
"dependencies": {
"papaparse": "^5.5.2",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
}
}
12 changes: 12 additions & 0 deletions clients/matching_component/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { DataExportPage } from '../src/matching/pages/DataExport/DataExportPage'
import { MatchingOverviewPage } from '../src/matching/MatchingOverviewPage'
import { ExtendedRouteObject } from '@/interfaces/extendedRouteObject'
import { Role } from '@tumaet/prompt-shared-state'
import { ParticipantsTablePage } from '../src/matching/pages/ParticipantsTable/ParticipantsTablePage'
import { DataImportPage } from '../src/matching/pages/DataImport/DataImportPage'

const routes: ExtendedRouteObject[] = [
{
Expand All @@ -14,6 +16,16 @@ const routes: ExtendedRouteObject[] = [
element: <DataExportPage />,
requiredPermissions: [Role.PROMPT_ADMIN, Role.COURSE_LECTURER],
},
{
path: '/re-import',
element: <DataImportPage />,
requiredPermissions: [Role.PROMPT_ADMIN, Role.COURSE_LECTURER],
},
{
path: '/participants',
element: <ParticipantsTablePage />,
requiredPermissions: [Role.PROMPT_ADMIN, Role.COURSE_LECTURER],
},
// Add more routes here as needed
]

Expand Down
10 changes: 9 additions & 1 deletion clients/matching_component/sidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { Puzzle } from 'lucide-react'
import { SidebarMenuItemProps } from '@/interfaces/sidebar'
import { Role } from '@tumaet/prompt-shared-state'

const sidebarItems: SidebarMenuItemProps = {
title: 'TemplateComponent',
icon: <Puzzle />,
requiredPermissions: [Role.PROMPT_ADMIN, Role.COURSE_LECTURER],
goToPath: '',
subitems: [],
subitems: [
{
title: 'Participants',
goToPath: '/participants',
requiredPermissions: [Role.PROMPT_ADMIN, Role.COURSE_LECTURER],
},
],
}

export default sidebarItems
106 changes: 70 additions & 36 deletions clients/matching_component/src/matching/MatchingOverviewPage.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import { ManagementPageHeader } from '@/components/ManagementPageHeader'
import { UploadButton } from './components/UploadButton'
import { FileUp, Loader2, UserRoundCheck } from 'lucide-react'
import { ClipboardList, FileUp, Loader2, UserRoundCheck } from 'lucide-react'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { CoursePhaseParticipationWithStudent } from '@tumaet/prompt-shared-state'
import { useQuery } from '@tanstack/react-query'
import { getCoursePhaseParticipations } from '@/network/queries/getCoursePhaseParticipations'
import { useMatchingStore } from './zustand/useMatchingStore'
import { useEffect } from 'react'
import { ErrorPage } from '@/components/ErrorPage'
import { useUploadAndParseXLSX } from './hooks/useUploadAndParseXLSX'
import { useUploadAndParseCSV } from './hooks/useUploadAndParseCSV'
import { Separator } from '@/components/ui/separator'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'

export const MatchingOverviewPage = (): JSX.Element => {
const { phaseId } = useParams<{ phaseId: string }>()
const navigate = useNavigate()
const path = useLocation().pathname

const { setParticipations } = useMatchingStore()
const { parseFileXLSX } = useUploadAndParseXLSX()
const { parseFileCSV } = useUploadAndParseCSV()

const {
data: coursePhaseParticipations,
Expand Down Expand Up @@ -42,41 +49,68 @@ export const MatchingOverviewPage = (): JSX.Element => {
<Loader2 className='h-12 w-12 animate-spin text-primary' />
</div>
) : (
<div className='grid md:grid-cols-2 gap-8'>
<section className='space-y-4'>
<h2 className='text-2xl font-bold flex items-center'>
<span className='bg-primary text-primary-foreground rounded-full w-8 h-8 flex items-center justify-center mr-2'>
1
</span>
Data Export
</h2>
<UploadButton
title='Export Data for TUM Matching'
description='Upload the file which you have received from TUM Matching to enter the ranks.'
icon={<FileUp className='h-6 w-6 mr-2' />}
onUploadFinish={() => {
navigate(`${path}/export`)
}}
acceptedFileTypes={['.xlsx']}
/>
</section>
<section className='space-y-4'>
<h2 className='text-2xl font-bold flex items-center'>
<span className='bg-primary text-primary-foreground rounded-full w-8 h-8 flex items-center justify-center mr-2'>
2
</span>
Data Re-Import
</h2>
<UploadButton
title='Re-Import Assigned Students'
description='Upload the students that the TUM Matching System has assigned to this course.'
icon={<UserRoundCheck className='h-6 w-6 mr-2' />}
onUploadFinish={() => {
navigate(`${path}/re-import`)
}}
acceptedFileTypes={['.xlsx']}
/>
</section>
<div>
<div className='grid md:grid-cols-2 gap-8'>
<section className='space-y-4'>
<h2 className='text-2xl font-bold flex items-center'>
<span className='bg-primary text-primary-foreground rounded-full w-8 h-8 flex items-center justify-center mr-2'>
1
</span>
Data Export
</h2>
<UploadButton
title='Export Data for TUM Matching'
description='Upload the file which you have received from TUM Matching to enter the ranks.'
icon={<FileUp className='h-6 w-6 mr-2' />}
onUploadFinish={() => {
navigate(`${path}/export`)
}}
onUploadFunction={parseFileXLSX}
acceptedFileTypes={['.xlsx']}
/>
</section>
<section className='space-y-4'>
<h2 className='text-2xl font-bold flex items-center'>
<span className='bg-primary text-primary-foreground rounded-full w-8 h-8 flex items-center justify-center mr-2'>
2
</span>
Data Re-Import
</h2>
<UploadButton
title='Re-Import Assigned Students'
description='Upload the students that the TUM Matching System has assigned to this course.'
icon={<UserRoundCheck className='h-6 w-6 mr-2' />}
onUploadFinish={() => {
navigate(`${path}/re-import`)
}}
onUploadFunction={parseFileCSV}
acceptedFileTypes={['.csv']}
/>
</section>
</div>
<Separator className='mt-16 mb-16' />
<div>
<Card className='w-[350px]'>
<CardHeader>
<CardTitle className='text-2xl font-bold flex items-center'>
<span className='bg-primary text-primary-foreground rounded-full w-8 h-8 flex items-center justify-center mr-2'>
3
</span>
Manual Data Inspection
</CardTitle>
<CardDescription>
Review and manage participants manually. This option allows you to inspect, pass,
or drop students outside of the automated import/export process.
</CardDescription>
</CardHeader>
<CardContent>
<Button onClick={() => navigate(`${path}/participants`)}>
<ClipboardList className='h-5 w-5 mr-2' />
Manual Inspection
</Button>
</CardContent>
</Card>
</div>
</div>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { type ReactNode, useRef, useState } from 'react'
import { UploadCloud, Loader2 } from 'lucide-react'
import { useUploadAndParse } from '../hooks/useUploadAndParse'
import { useMatchingStore } from '../zustand/useMatchingStore'

interface UploadButtonProps {
title: string
description: string
icon: ReactNode
onUploadFinish: () => void
onUploadFunction: (file: File) => Promise<void>
acceptedFileTypes: string[]
}

Expand All @@ -18,14 +18,14 @@ export const UploadButton = ({
description,
icon,
onUploadFinish,
onUploadFunction,
acceptedFileTypes,
}: UploadButtonProps): JSX.Element => {
const fileInputRef = useRef<HTMLInputElement>(null)
const [dragActive, setDragActive] = useState(false)
const [isUploading, setIsUploading] = useState(false)
const [error, setError] = useState<string | null>(null) // State to store error messages

const { parseFile } = useUploadAndParse()
const { setUploadedFile } = useMatchingStore()

const handleUpload = async (file: File) => {
Expand All @@ -34,7 +34,7 @@ export const UploadButton = ({
setUploadedFile(file)
setTimeout(async () => {
try {
await parseFile(file)
await onUploadFunction(file)
onUploadFinish()
} catch (err: any) {
console.error('Failed to parse file:', err)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import Papa from 'papaparse'
import { useMatchingStore } from '../zustand/useMatchingStore'
import { UploadedStudent } from '../interfaces/UploadedStudent'

/**
* This hook returns a function that, when called with a CSV File,
* parses it and updates the Zustand store with the uploaded data.
*/
export const useUploadAndParseCSV = () => {
const { setUploadedData } = useMatchingStore()

/**
* Parses a CSV file expecting the following headers:
* "Students first name", "Students last name", "Students matriculation number"
*
* Returns a Promise of the parsed data. Will throw an error if:
* - The file is empty or cannot be read
* - The header row is missing or incorrect
* - Required data fields in rows are missing
*/
const parseFileCSV = async (file: File): Promise<void> => {
try {
// 1. Parse the CSV using Papa Parse in 'header' mode
// Setting `dynamicTyping: false` ensures all values are treated as strings
// so leading zeros are preserved.
const result = await new Promise<Papa.ParseResult>((resolve, reject) => {
Papa.parse(file, {
header: true,
skipEmptyLines: true,
dynamicTyping: false, // ensures leading zeros remain as strings
complete: resolve,
error: reject,
})
})

// 2. Basic validations
if (!result || !result.data || result.data.length === 0) {
throw new Error('No data found in the CSV file.')
}

// 3. Validate the header row
const expectedHeaders = [
'Students first name',
'Students last name',
'Students matriculation number',
]
const missingHeaders = expectedHeaders.filter((h) => !result.meta.fields?.includes(h))
if (missingHeaders.length > 0) {
throw new Error(`Missing headers: ${missingHeaders.join(', ')}`)
}

// 4. Parse each row and map it to the UploadedStudent interface
const parsedData: UploadedStudent[] = result.data.map(
(row: Record<string, unknown>, rowIndex: number) => {
const firstName = row['Students first name']
const lastName = row['Students last name']
const matriculationNumber = row['Students matriculation number']

if (!firstName || !lastName || !matriculationNumber) {
throw new Error(
`Row ${rowIndex + 2} is missing required fields (first name, last name, or matriculation number).`,
)
}

return {
firstName: String(firstName),
lastName: String(lastName),
matriculationNumber: String(matriculationNumber),
}
},
)

// 5. Update Zustand store with parsed data
setUploadedData(parsedData)
} catch (error) {
console.error('Error uploading and parsing CSV file:', error)
throw error // Re-throw to let the caller handle it
}
}

return { parseFileCSV }
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { UploadedStudent } from '../interfaces/UploadedStudent'
* This hook returns a function that, when called with a File,
* parses the Excel and updates the Zustand store with the uploaded data.
*/
export const useUploadAndParse = () => {
export const useUploadAndParseXLSX = () => {
const { setUploadedData } = useMatchingStore()

/**
Expand All @@ -18,7 +18,7 @@ export const useUploadAndParse = () => {
* - The header row is missing or incorrect
* - Required data fields in rows are missing
*/
const parseFile = async (file: File): Promise<UploadedStudent[]> => {
const parseFileXLSX = async (file: File): Promise<void> => {
try {
// 1. Parse the workbook from the file
const arrayBuffer = await file.arrayBuffer()
Expand Down Expand Up @@ -88,15 +88,12 @@ export const useUploadAndParse = () => {

// 7. Update Zustand store with parsed data
setUploadedData(parsedData)

// 8. Return the parsed data so the caller can also use it
return parsedData
} catch (error) {
// Handle any parsing or data-structure errors
console.error('Error uploading and parsing Excel file:', error)
throw error // Re-throw to let caller handle it
}
}

return { parseFile }
return { parseFileXLSX }
}
Loading

0 comments on commit cbaf602

Please sign in to comment.