Skip to content

Assessment: introduce deadline for assessment phase #607

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

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { assessmentAxiosInstance } from '../assessmentServerConfig'

export const updateDeadline = async (coursePhaseID: string, deadline: Date): Promise<void> => {
try {
await assessmentAxiosInstance.put(
`assessment/api/course_phase/${coursePhaseID}/deadline`,
{ deadline: deadline },
{
headers: {
'Content-Type': 'application/json',
},
},
)
} catch (err) {
console.error(err)
throw err
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { assessmentAxiosInstance } from '../assessmentServerConfig'

export const getDeadline = async (coursePhaseID: string): Promise<Date> => {
const response = await assessmentAxiosInstance.get<Date>(
`assessment/api/course_phase/${coursePhaseID}/deadline`,
{
headers: {
'Content-Type': 'application/json',
},
},
)
return response.data
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { Loader2 } from 'lucide-react'
import { useEffect } from 'react'
import { useParams } from 'react-router-dom'

import { useQuery } from '@tanstack/react-query'

import { CoursePhaseParticipationsWithResolution } from '@tumaet/prompt-shared-state'
import { ErrorPage } from '@tumaet/prompt-ui-components'
import { ErrorPage, LoadingPage } from '@tumaet/prompt-ui-components'
import { getCoursePhaseParticipations } from '@/network/queries/getCoursePhaseParticipations'

import { useGetAllCategoriesWithCompetencies } from './hooks/useGetAllCategoriesWithCompetencies'
import { useGetAllScoreLevels } from './hooks/useGetAllScoreLevels'
import { useGetDeadline } from './hooks/useGetDeadline'

import { useParticipationStore } from '../zustand/useParticipationStore'
import { useCategoryStore } from '../zustand/useCategoryStore'
import { useScoreLevelStore } from '../zustand/useScoreLevelStore'
import { useDeadlineStore } from '../zustand/useDeadlineStore'

interface AssessmentDataShellProps {
children: React.ReactNode
Expand All @@ -24,6 +25,7 @@ export const AssessmentDataShell = ({ children }: AssessmentDataShellProps) => {
const { setParticipations } = useParticipationStore()
const { setCategories } = useCategoryStore()
const { setScoreLevels } = useScoreLevelStore()
const { setDeadline } = useDeadlineStore()

const {
data: coursePhaseParticipations,
Expand All @@ -49,14 +51,26 @@ export const AssessmentDataShell = ({ children }: AssessmentDataShellProps) => {
refetch: refetchScoreLevels,
} = useGetAllScoreLevels()

const isError = isParticipationsError || isCategoriesError || isScoreLevelsError
const {
data: deadline,
isPending: isDeadlinePending,
isError: isDeadlineError,
refetch: refetchDeadline,
} = useGetDeadline()

const isError =
isParticipationsError || isCategoriesError || isScoreLevelsError || isDeadlineError
const isPending =
isCoursePhaseParticipationsPending || isCategoriesPending || isScoreLevelsPending
isCoursePhaseParticipationsPending ||
isCategoriesPending ||
isScoreLevelsPending ||
isDeadlinePending

const refetch = () => {
refetchCoursePhaseParticipations()
refetchCategories()
refetchScoreLevels()
refetchDeadline()
}

useEffect(() => {
Expand All @@ -77,17 +91,13 @@ export const AssessmentDataShell = ({ children }: AssessmentDataShellProps) => {
}
}, [scoreLevels, setScoreLevels])

useEffect(() => {
if (deadline && deadline != null) {
setDeadline(deadline)
}
}, [deadline, setDeadline])

return (
<>
{isError ? (
<ErrorPage onRetry={refetch} />
) : isPending ? (
<div className='flex justify-center items-center h-64'>
<Loader2 className='h-12 w-12 animate-spin text-primary' />
</div>
) : (
<>{children}</>
)}
</>
<>{isError ? <ErrorPage onRetry={refetch} /> : isPending ? <LoadingPage /> : <>{children}</>}</>
)
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Loader2 } from 'lucide-react'
import { useParams } from 'react-router-dom'
import { useMemo } from 'react'

import { ErrorPage } from '@tumaet/prompt-ui-components'
import { ErrorPage, LoadingPage } from '@tumaet/prompt-ui-components'

import { useGetStudentAssessment } from './hooks/useGetStudentAssessment'
import { CategoryAssessment } from './components/CategoryAssessment'
Expand Down Expand Up @@ -36,12 +35,7 @@ export const AssessmentPage = (): JSX.Element => {
}, [categories, studentAssessment?.assessments?.length])

if (isStudentAssessmentError) return <ErrorPage onRetry={refetchStudentAssessment} />
if (isStudentAssessmentPending)
return (
<div className='flex justify-center items-center h-64'>
<Loader2 className='h-12 w-12 animate-spin text-primary' />
</div>
)
if (isStudentAssessmentPending) return <LoadingPage />

if (!studentAssessment) {
return (
Expand Down Expand Up @@ -79,7 +73,6 @@ export const AssessmentPage = (): JSX.Element => {

<AssessmentCompletion
studentAssessment={studentAssessment}
deadline='19.06.2025'
completed={studentAssessment.assessmentCompletion.completed}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { useState } from 'react'
import { useParams } from 'react-router-dom'
import { Lock, Unlock } from 'lucide-react'

import { format } from 'date-fns'

import {
Button,
Card,
Expand All @@ -27,20 +29,21 @@ import { AssessmentCompletionDialog } from './components/AssessmentCompletionDia
import { useCreateOrUpdateAssessmentCompletion } from './hooks/useCreateOrUpdateAssessmentCompletion'
import { useMarkAssessmentAsComplete } from './hooks/useMarkAssessmentAsComplete'
import { useUnmarkAssessmentAsCompleted } from './hooks/useUnmarkAssessmentAsCompleted'
import { useDeadlineStore } from '../../../../zustand/useDeadlineStore'

interface AssessmentFeedbackProps {
studentAssessment: StudentAssessment
deadline?: string
completed?: boolean
}

export function AssessmentCompletion({
studentAssessment,
deadline = '19.06.2025',
completed = false,
}: AssessmentFeedbackProps) {
const { phaseId } = useParams<{ phaseId: string }>()

const { deadline } = useDeadlineStore()

const [generalRemarks, setGeneralRemarks] = useState(
studentAssessment.assessmentCompletion?.comment || '',
)
Expand Down Expand Up @@ -226,7 +229,12 @@ export function AssessmentCompletion({
)}

<div className='flex justify-between items-center mt-8'>
<div className='text-muted-foreground'>Deadline: {deadline}</div>
{deadline && (
<div className='text-muted-foreground'>
Deadline: {deadline ? format(new Date(deadline), 'dd.MM.yyyy') : 'No deadline set'}
</div>
)}

<Button size='sm' disabled={isPending} onClick={handleButtonClick}>
{studentAssessment.assessmentCompletion.completed ? (
<span className='flex items-center gap-1'>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ManagementPageHeader } from '@tumaet/prompt-ui-components'

import { DeadlineSelection } from './components/DeadlineSelection/DeadlineSelection'
import { AssessmentTemplateSelection } from './components/AssessmentTemplateSelection/AssessmentTemplateSelection'
import { CategoryList } from './components/CategoryList'
import { CreateCategoryForm } from './components/CreateCategoryForm'
Expand All @@ -7,7 +9,11 @@ export const SettingsPage = (): JSX.Element => {
return (
<div className='space-y-4'>
<ManagementPageHeader>Assessment Settings</ManagementPageHeader>
<AssessmentTemplateSelection />
<div className='grid xl:grid-cols-3 gap-4'>
<AssessmentTemplateSelection />
<DeadlineSelection />
</div>

<CategoryList />
<CreateCategoryForm />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ export const AssessmentTemplateSelection = () => {
}

return (
<Card className='shadow-sm transition-all hover:shadow-md'>
<CardHeader className='pb-4'>
<Card className='xl:col-span-2 shadow-sm transition-all hover:shadow-md'>
<CardHeader>
<CardTitle className='flex items-center gap-2'>
<FileText className='h-5 w-5' />
Assessment Template
Template
</CardTitle>
</CardHeader>
<CardContent className='space-y-4'>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useState, useEffect } from 'react'
import { format } from 'date-fns'

import { Calendar } from 'lucide-react'

import {
DatePicker,
Button,
Card,
CardContent,
CardHeader,
CardTitle,
Label,
} from '@tumaet/prompt-ui-components'

import { useUpdateDeadline } from './hooks/useUpdateDeadline'
import { useDeadlineStore } from '../../../../zustand/useDeadlineStore'

export const DeadlineSelection = (): JSX.Element => {
const [deadline, setDeadline] = useState<Date | undefined>(undefined)
const [error, setError] = useState<string | null>(null)

const { deadline: currentDeadline } = useDeadlineStore()

useEffect(() => {
if (currentDeadline) {
setDeadline(new Date(currentDeadline))
}
}, [currentDeadline])

const updateDeadlineMutation = useUpdateDeadline(setError)
const handleDeadlineUpdate = () => {
if (deadline) {
updateDeadlineMutation.mutate(deadline)
}
}

return (
<Card>
<CardHeader>
<CardTitle className='flex items-center gap-2'>
<Calendar className='h-5 w-5' />
Deadline
</CardTitle>
</CardHeader>
<CardContent className='space-y-4'>
<div className='space-y-2'>
<Label>Select Deadline Date</Label>

<div className='flex items-center gap-2'>
<DatePicker
date={deadline}
onSelect={(date) =>
setDeadline(date ? new Date(format(date, 'yyyy-MM-dd')) : undefined)
}
/>

<Button
onClick={handleDeadlineUpdate}
disabled={!deadline || updateDeadlineMutation.isPending}
>
{updateDeadlineMutation.isPending ? 'Updating...' : 'Update Deadline'}
</Button>
</div>
</div>

<div className='bg-blue-50 p-3 rounded-lg'>
<p className='text-sm text-blue-800'>
<strong>Current deadline:</strong>{' '}
{currentDeadline ? format(new Date(currentDeadline), 'dd.MM.yyyy') : 'No deadline set'}
<p className='text-sm text-blue-600 mt-1'>
Once a deadline is set, assessors cannot unmark their assessment as final anymore.
</p>
</p>
</div>

{error && <div className='text-red-600 text-sm'>{error}</div>}

{updateDeadlineMutation.isSuccess && (
<div className='text-green-600 text-sm'>Deadline updated successfully!</div>
)}
</CardContent>
</Card>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useParams } from 'react-router-dom'

import { useMutation, useQueryClient } from '@tanstack/react-query'

import { updateDeadline } from '../../../../../network/mutations/updateDeadline'

export const useUpdateDeadline = (setError: (error: string | null) => void) => {
const { phaseId } = useParams<{ phaseId: string }>()
const queryClient = useQueryClient()

return useMutation({
mutationFn: (request: Date) => updateDeadline(phaseId ?? '', request),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['deadline'] })
setError(null)
},
onError: (error: any) => {
if (error?.response?.data?.error) {
const serverError = error.response.data?.error
setError(serverError)
} else {
setError('An unexpected error occurred. Please try again.')
}
},
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useQuery } from '@tanstack/react-query'
import { useParams } from 'react-router-dom'
import { getDeadline } from '../../network/queries/getDeadline'

export const useGetDeadline = () => {
const { phaseId } = useParams<{ phaseId: string }>()

return useQuery<Date>({
queryKey: ['deadline', phaseId],
queryFn: () => getDeadline(phaseId ?? ''),
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { create } from 'zustand'

export interface DeadlineStore {
deadline: Date | undefined
setDeadline: (deadline: Date) => void
}

export const useDeadlineStore = create<DeadlineStore>((set) => ({
deadline: undefined,
setDeadline: (deadline) => set({ deadline }),
}))
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package coursePhaseConfigDTO

import "time"

// UpdateDeadlineRequest represents the request to update a course phase deadline
type UpdateDeadlineRequest struct {
Deadline time.Time `json:"deadline"`
}

// DeadlineResponse represents the response when getting a course phase deadline
type DeadlineResponse struct {
Deadline *time.Time `json:"deadline"`
}
Loading
Loading