2026-06-07 18:43:05 +01:00
|
|
|
<script module lang="ts">
|
|
|
|
|
let pending: Promise<typeof import('@cio/ui/base/chart')> | null = null;
|
|
|
|
|
function loadChart() {
|
|
|
|
|
pending ??= import('@cio/ui/base/chart');
|
|
|
|
|
return pending;
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
2026-03-09 11:18:22 +01:00
|
|
|
<script lang="ts">
|
2026-06-07 18:43:05 +01:00
|
|
|
import type * as Chart from '@cio/ui/base/chart';
|
2026-05-18 03:47:22 +01:00
|
|
|
import { Spinner } from '@cio/ui/base/spinner';
|
|
|
|
|
import { Empty } from '@cio/ui/custom/empty';
|
|
|
|
|
import BarChartIcon from '@lucide/svelte/icons/bar-chart-3';
|
|
|
|
|
import { AnalyticsPanelCard } from '$features/analytics';
|
2026-03-09 11:18:22 +01:00
|
|
|
import type { CourseAnalytics } from '$features/course/utils/types';
|
|
|
|
|
import { t } from '$lib/utils/functions/translations';
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
courseAnalytics?: CourseAnalytics | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type DistributionChartData = {
|
|
|
|
|
group: string;
|
|
|
|
|
value: number;
|
|
|
|
|
color: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let { courseAnalytics = null }: Props = $props();
|
|
|
|
|
|
2026-03-13 09:53:05 +01:00
|
|
|
const progressChartConfig = $derived({
|
|
|
|
|
students: {
|
|
|
|
|
label: $t('analytics.number_of_students'),
|
|
|
|
|
color: 'var(--chart-1)'
|
|
|
|
|
}
|
|
|
|
|
} satisfies Chart.ChartConfig);
|
|
|
|
|
|
|
|
|
|
const gradeChartConfig = $derived({
|
|
|
|
|
students: {
|
|
|
|
|
label: $t('analytics.number_of_students'),
|
|
|
|
|
color: 'var(--chart-2)'
|
|
|
|
|
}
|
|
|
|
|
} satisfies Chart.ChartConfig);
|
2026-03-09 11:18:22 +01:00
|
|
|
|
2026-05-18 03:47:22 +01:00
|
|
|
const hasStudentData = $derived(Boolean(courseAnalytics?.students?.length));
|
2026-03-09 11:18:22 +01:00
|
|
|
|
2026-05-18 03:47:22 +01:00
|
|
|
const progressChartData = $derived<DistributionChartData[]>(
|
2026-03-09 11:18:22 +01:00
|
|
|
courseAnalytics?.students
|
|
|
|
|
? [
|
|
|
|
|
{
|
|
|
|
|
group: $t('analytics.progress_80_plus'),
|
|
|
|
|
value: courseAnalytics.students.filter((s) => s.progressPercentage >= 80).length,
|
|
|
|
|
color: '#10b981'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
group: $t('analytics.progress_60_79'),
|
|
|
|
|
value: courseAnalytics.students.filter((s) => s.progressPercentage >= 60 && s.progressPercentage < 80)
|
|
|
|
|
.length,
|
|
|
|
|
color: '#f59e0b'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
group: $t('analytics.progress_below_60'),
|
|
|
|
|
value: courseAnalytics.students.filter((s) => s.progressPercentage < 60).length,
|
|
|
|
|
color: '#ef4444'
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
: []
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-18 03:47:22 +01:00
|
|
|
const gradeChartData = $derived<DistributionChartData[]>(
|
2026-03-09 11:18:22 +01:00
|
|
|
courseAnalytics?.students
|
|
|
|
|
? [
|
|
|
|
|
{
|
|
|
|
|
group: $t('analytics.grade_90_plus'),
|
|
|
|
|
value: courseAnalytics.students.filter((s) => s.averageGrade >= 90).length,
|
|
|
|
|
color: '#10b981'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
group: $t('analytics.grade_80_89'),
|
|
|
|
|
value: courseAnalytics.students.filter((s) => s.averageGrade >= 80 && s.averageGrade < 90).length,
|
|
|
|
|
color: '#3b82f6'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
group: $t('analytics.grade_70_79'),
|
|
|
|
|
value: courseAnalytics.students.filter((s) => s.averageGrade >= 70 && s.averageGrade < 80).length,
|
|
|
|
|
color: '#f59e0b'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
group: $t('analytics.grade_below_70'),
|
|
|
|
|
value: courseAnalytics.students.filter((s) => s.averageGrade < 70).length,
|
|
|
|
|
color: '#ef4444'
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
: []
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-18 03:47:22 +01:00
|
|
|
const progressChartProps = $derived({
|
2026-03-09 11:18:22 +01:00
|
|
|
xAxis: {
|
|
|
|
|
label: $t('analytics.progress_range')
|
|
|
|
|
},
|
|
|
|
|
yAxis: {
|
|
|
|
|
label: $t('analytics.number_of_students')
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-18 03:47:22 +01:00
|
|
|
const gradeChartProps = $derived({
|
2026-03-09 11:18:22 +01:00
|
|
|
xAxis: {
|
|
|
|
|
label: $t('analytics.grade_range')
|
|
|
|
|
},
|
|
|
|
|
yAxis: {
|
|
|
|
|
label: $t('analytics.number_of_students')
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-18 03:47:22 +01:00
|
|
|
const progressSeries = $derived([
|
2026-03-13 09:53:05 +01:00
|
|
|
{
|
|
|
|
|
key: 'students',
|
|
|
|
|
value: 'value',
|
|
|
|
|
label: progressChartConfig.students.label,
|
|
|
|
|
color: 'var(--color-students)'
|
|
|
|
|
}
|
|
|
|
|
]);
|
2026-03-09 11:18:22 +01:00
|
|
|
|
2026-05-18 03:47:22 +01:00
|
|
|
const gradeSeries = $derived([
|
2026-03-13 09:53:05 +01:00
|
|
|
{
|
|
|
|
|
key: 'students',
|
|
|
|
|
value: 'value',
|
|
|
|
|
label: gradeChartConfig.students.label,
|
|
|
|
|
color: 'var(--color-students)'
|
|
|
|
|
}
|
|
|
|
|
]);
|
2026-05-18 03:47:22 +01:00
|
|
|
|
|
|
|
|
const chartsLoading = $derived(!courseAnalytics);
|
2026-03-09 11:18:22 +01:00
|
|
|
</script>
|
|
|
|
|
|
2026-05-18 03:47:22 +01:00
|
|
|
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
|
|
|
|
<AnalyticsPanelCard
|
|
|
|
|
title={$t('analytics.student_progress_distribution')}
|
|
|
|
|
description={$t('analytics.course_metrics.progress_distribution_card_description')}
|
|
|
|
|
>
|
|
|
|
|
{#snippet children()}
|
|
|
|
|
{#if chartsLoading}
|
|
|
|
|
<div class="flex h-[280px] items-center justify-center">
|
|
|
|
|
<Spinner class="ui:text-muted-foreground size-6" />
|
|
|
|
|
</div>
|
|
|
|
|
{:else if hasStudentData}
|
2026-06-07 18:43:05 +01:00
|
|
|
{#await loadChart() then C}
|
|
|
|
|
<C.ChartContainer class="h-[280px] w-full" config={progressChartConfig}>
|
|
|
|
|
<C.BarChart
|
|
|
|
|
data={progressChartData}
|
|
|
|
|
x="group"
|
|
|
|
|
axis="x"
|
|
|
|
|
props={progressChartProps}
|
|
|
|
|
series={progressSeries}
|
|
|
|
|
/>
|
|
|
|
|
</C.ChartContainer>
|
|
|
|
|
{/await}
|
2026-05-18 03:47:22 +01:00
|
|
|
{:else}
|
|
|
|
|
<Empty icon={BarChartIcon} title={$t('analytics.no_progress_data')} class="h-[280px]" />
|
|
|
|
|
{/if}
|
|
|
|
|
{/snippet}
|
|
|
|
|
</AnalyticsPanelCard>
|
2026-03-09 11:18:22 +01:00
|
|
|
|
2026-05-18 03:47:22 +01:00
|
|
|
<AnalyticsPanelCard
|
|
|
|
|
title={$t('analytics.grade_distribution')}
|
|
|
|
|
description={$t('analytics.course_metrics.grade_distribution_card_description')}
|
|
|
|
|
>
|
|
|
|
|
{#snippet children()}
|
|
|
|
|
{#if chartsLoading}
|
|
|
|
|
<div class="flex h-[280px] items-center justify-center">
|
|
|
|
|
<Spinner class="ui:text-muted-foreground size-6" />
|
|
|
|
|
</div>
|
|
|
|
|
{:else if hasStudentData}
|
2026-06-07 18:43:05 +01:00
|
|
|
{#await loadChart() then C}
|
|
|
|
|
<C.ChartContainer class="h-[280px] w-full" config={gradeChartConfig}>
|
|
|
|
|
<C.BarChart data={gradeChartData} x="group" axis="x" props={gradeChartProps} series={gradeSeries} />
|
|
|
|
|
</C.ChartContainer>
|
|
|
|
|
{/await}
|
2026-05-18 03:47:22 +01:00
|
|
|
{:else}
|
|
|
|
|
<Empty icon={BarChartIcon} title={$t('analytics.no_grade_data')} class="h-[280px]" />
|
|
|
|
|
{/if}
|
|
|
|
|
{/snippet}
|
|
|
|
|
</AnalyticsPanelCard>
|
2026-03-09 11:18:22 +01:00
|
|
|
</div>
|