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

enhance(apps/analytics): add illustrations for performance rates on activities and instances #4398

Merged
merged 9 commits into from
Dec 18, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Button } from '@uzh-bf/design-system'
import { useTranslations } from 'next-intl'
import { SetStateAction } from 'react'

function ActivitiesElementsSwitch({
type,
setType,
}: {
type: 'activity' | 'instance'
setType: (value: SetStateAction<'activity' | 'instance'>) => void
}) {
const t = useTranslations()

return (
<div className="flex flex-row">
<Button
basic
onClick={() => setType('activity')}
className={{
root: `py-0.25 rounded-l border !border-r-0 border-solid px-2 ${type === 'activity' ? 'bg-primary-100 border-primary-100 text-white' : ''}`,
}}
>
{t('manage.analytics.activities')}
</Button>
<Button
basic
onClick={() => setType('instance')}
className={{
root: `rounded-r border !border-l-0 border-solid px-2 py-0.5 ${type === 'instance' ? 'bg-primary-100 border-primary-100 text-white' : ''}`,
}}
>
{t('manage.analytics.elements')}
</Button>
</div>
sjschlapbach marked this conversation as resolved.
Show resolved Hide resolved
)
}

export default ActivitiesElementsSwitch
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ActivityType } from '@klicker-uzh/graphql/dist/ops'
import { SelectField } from '@uzh-bf/design-system'
import { useTranslations } from 'next-intl'
import { Dispatch, SetStateAction } from 'react'

function PerformanceActivityTypeFilter({
activityType,
setActivityType,
}: {
activityType: ActivityType | 'all'
setActivityType: Dispatch<SetStateAction<'all' | ActivityType>>
}) {
const t = useTranslations()

return (
<SelectField
label={t('manage.analytics.activityType')}
items={[
{ value: 'all', label: t('manage.analytics.allActivityTypes') },
{
value: ActivityType.PracticeQuiz,
label: t('shared.generic.practiceQuizzes'),
},
{
value: ActivityType.MicroLearning,
label: t('shared.generic.microlearnings'),
},
]}
sjschlapbach marked this conversation as resolved.
Show resolved Hide resolved
value={activityType}
onChange={(value) => setActivityType(value as ActivityType | 'all')}
className={{ select: { root: 'w-52', trigger: 'h-8' } }}
sjschlapbach marked this conversation as resolved.
Show resolved Hide resolved
/>
)
}

export default PerformanceActivityTypeFilter
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { SelectField } from '@uzh-bf/design-system'
import { useTranslations } from 'next-intl'
import { Dispatch, SetStateAction } from 'react'

function PerformanceAttemptsFilter({
attemptsType,
setAttemptsType,
}: {
attemptsType: 'first' | 'last' | 'total'
setAttemptsType: Dispatch<SetStateAction<'first' | 'last' | 'total'>>
}) {
const t = useTranslations()

return (
<SelectField
label={t('manage.analytics.answers')}
items={[
{ value: 'total', label: t('manage.analytics.allAttempts') },
{ value: 'first', label: t('manage.analytics.firstAttempts') },
{ value: 'last', label: t('manage.analytics.lastAttempts') },
]}
value={attemptsType}
onChange={(value) => setAttemptsType(value as 'first' | 'last' | 'total')}
className={{ select: { root: 'w-40', trigger: 'h-8' } }}
/>
)
}

export default PerformanceAttemptsFilter
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ElementType } from '@klicker-uzh/graphql/dist/ops'
import { SelectField } from '@uzh-bf/design-system'
import { useTranslations } from 'next-intl'
import { Dispatch, SetStateAction } from 'react'

function PerformanceElementTypeFilter({
elementType,
setElementType,
}: {
elementType: ElementType | 'all'
setElementType: Dispatch<SetStateAction<ElementType | 'all'>>
}) {
const t = useTranslations()

return (
<SelectField
label={t('manage.analytics.elementType')}
items={[
{ value: 'all', label: t('manage.analytics.allElementTypes') },
...Object.values(ElementType).map((value) => ({
value,
label: t(`shared.${value}.typeLabel`),
})),
]}
value={elementType}
onChange={(value) => setElementType(value as ElementType | 'all')}
className={{ select: { root: 'w-52', trigger: 'h-8' } }}
/>
sjschlapbach marked this conversation as resolved.
Show resolved Hide resolved
)
}

export default PerformanceElementTypeFilter
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { faX } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
ActivityPerformance,
ActivityType,
ElementType,
InstancePerformance,
} from '@klicker-uzh/graphql/dist/ops'
import usePerformanceRates from '@lib/hooks/usePerformanceRates'
import usePerformanceSearch from '@lib/hooks/usePerformanceSearch'
import { Button, H2, UserNotification } from '@uzh-bf/design-system'
import { useTranslations } from 'next-intl'
import { useState } from 'react'
import { Legend } from 'recharts'
import ActivitiesElementsSwitch from './ActivitiesElementsSwitch'
import PerformanceActivityTypeFilter from './PerformanceActivityTypeFilter'
import PerformanceAttemptsFilter from './PerformanceAttemptsFilter'
import PerformanceElementTypeFilter from './PerformanceElementTypeFilter'
import PerformanceRatesBarChart from './PerformanceRatesBarChart'
import PerformanceSearchField from './PerformanceSearchField'

interface PerformanceRatesProps {
activityPerformances: ActivityPerformance[]
instancePerformances: InstancePerformance[]
}

function PerformanceRates({
activityPerformances,
instancePerformances,
}: PerformanceRatesProps) {
const t = useTranslations()
const chartColors = {
correct: '#064e3b',
partial: '#f59e0b',
incorrect: '#cc0000',
}
const defaultFilters = {
type: 'activity' as 'activity' | 'instance',
attemptsType: 'total' as 'first' | 'last' | 'total',
activityType: 'all' as ActivityType | 'all',
elementType: 'all' as ElementType | 'all',
}

// define parameters for filtering and searching
const [type, setType] = useState<'activity' | 'instance'>(defaultFilters.type)
const [attemptsType, setAttemptsType] = useState<'first' | 'last' | 'total'>(
defaultFilters.attemptsType
)
const [activityType, setActivityType] = useState<ActivityType | 'all'>(
defaultFilters.activityType
)
const [elementType, setElementType] = useState<ElementType | 'all'>(
defaultFilters.elementType
)
const [search, setSearch] = useState<string>('')

// apply the search hook
const searchResults = usePerformanceSearch(
activityPerformances,
instancePerformances,
type,
search
)

// if any filters are provided, narrow down the performance rate entries shown
const entries = usePerformanceRates(
searchResults,
activityType,
elementType,
attemptsType
)

return (
<div className="border-uzh-grey-80 rounded-xl border border-solid p-3">
<div className="flex flex-row items-center justify-between">
<div className="mb-2 flex flex-row gap-8">
<H2>{t('manage.analytics.activityElementPerformanceRates')}</H2>
<ActivitiesElementsSwitch type={type} setType={setType} />
</div>
<Button
className={{ root: 'flex h-8 flex-row items-center gap-2' }}
disabled={
type === defaultFilters.type &&
attemptsType === defaultFilters.attemptsType &&
activityType === defaultFilters.activityType &&
elementType === defaultFilters.elementType
}
onClick={() => {
setType(defaultFilters.type)
setAttemptsType(defaultFilters.attemptsType)
setActivityType(defaultFilters.activityType)
setElementType(defaultFilters.elementType)
}}
>
<FontAwesomeIcon icon={faX} />
<div>{t('manage.analytics.resetSelectors')}</div>
</Button>
</div>
{type === 'activity' ? (
<div className="flex flex-row items-center gap-8">
<PerformanceAttemptsFilter
attemptsType={attemptsType}
setAttemptsType={setAttemptsType}
/>
<PerformanceActivityTypeFilter
activityType={activityType}
setActivityType={setActivityType}
/>
<PerformanceSearchField
type={type}
value={search}
onChange={(value) => setSearch(value)}
/>
</div>
) : (
<div className="flex flex-row items-center gap-8">
<PerformanceAttemptsFilter
attemptsType={attemptsType}
setAttemptsType={setAttemptsType}
/>
<PerformanceElementTypeFilter
elementType={elementType}
setElementType={setElementType}
/>
<PerformanceSearchField
type={type}
value={search}
onChange={(value) => setSearch(value)}
/>
</div>
)}
{entries.length > 0 ? (
<div className="relative">
<Legend
payload={[
{
value: t('manage.analytics.errorRate'),
color: chartColors.incorrect,
type: 'rect',
},
{
value: t('manage.analytics.partialRate'),
color: chartColors.partial,
type: 'rect',
},
{
value: t('manage.analytics.correctRate'),
color: chartColors.correct,
type: 'rect',
},
]}
wrapperStyle={{ top: 0, right: 0 }}
/>
<div className="flex flex-col pt-6">
{entries.length > 0 && (
<div className="max-h-[13rem] overflow-y-scroll">
{entries.map((progress) => (
<PerformanceRatesBarChart
key={`performance-rates-${progress.id}`}
title={progress.name}
rates={progress}
colors={chartColors}
/>
))}
</div>
)}
sjschlapbach marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
) : (
<UserNotification
type="info"
message={t('manage.analytics.noEntriesManageFilters')}
className={{ root: 'mt-4' }}
/>
)}
</div>
)
}

export default PerformanceRates
Loading
Loading