feat: setup dashboard analytics api (#604)

* feat: setup dashboard analytics api

* chore: requested pr fixes

* fix: dash analytics server side type

* refactor: update Avatar component usage across the dashboard

- Replaced custom Avatar component with the new @cio/ui/base/avatar implementation for consistency.
- Enhanced avatar rendering with fallback options and improved accessibility.
- Cleaned up unused Avatar component files and related functions.
- Updated various components to utilize the new Avatar structure, ensuring a uniform appearance and behavior across the application.

* chore: remove console.log statements from analytics routes and API calls

- Eliminated console.log statements for orgId and siteName in the dash analytics router and DashStatApi class to clean up the code and improve performance.

---------

Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Adeyemi Adetunji
2025-12-02 15:27:41 +01:00
committed by GitHub
co-authored by Rotimi Best
parent c7854efae3
commit 9a5aee1ee0
40 changed files with 573 additions and 276 deletions
@@ -10,14 +10,13 @@
import { t } from '$lib/utils/functions/translations';
import { calDateDiff } from '$lib/utils/functions/date';
import { getGreeting } from '$lib/utils/functions/date';
import { snackbar } from '$lib/components/Snackbar/store';
import { getAccessToken } from '$lib/utils/functions/supabase';
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
import type { OrganisationAnalytics } from '$lib/utils/types/analytics';
import { currentOrgPath } from '$lib/utils/store/org';
import { dashStatApi } from '$lib/features/org/api';
import { CreateCourseButton } from '$lib/features/course/components';
import Avatar from '$lib/components/Avatar/index.svelte';
import * as Avatar from '@cio/ui/base/avatar';
import { ActivityCard } from '$lib/components/Analytics';
import { shortenName } from '$lib/utils/functions/string';
import Progress from '$lib/components/Progress/index.svelte';
import { Button } from '@cio/ui/base/button';
import { WelcomeModal } from '$lib/features/onboarding/components';
@@ -25,60 +24,33 @@
import * as Page from '@cio/ui/base/page';
import { Empty } from '@cio/ui/custom/empty';
let dashAnalytics: OrganisationAnalytics | undefined = $state();
let hasFetched = $state(false);
const { data } = $props();
async function fetchDashAnalytics(orgId: string) {
if (!orgId || hasFetched) return;
hasFetched = true;
const accessToken = await getAccessToken();
try {
const response = await fetch('/api/analytics/dash', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: accessToken
},
body: JSON.stringify({ orgId })
});
if (!response.ok) {
console.error(response);
throw new Error('Failed to fetch analytics data');
}
dashAnalytics = (await response.json()) as OrganisationAnalytics;
} catch (error) {
snackbar.error('Failed to fetch analytics data');
}
}
const { enrollments, numberOfCourses, revenue, totalStudents, topCourses } = $derived(dashStatApi.stats);
$effect(() => {
fetchDashAnalytics($currentOrg.id);
dashStatApi.fetchOrgStats({ siteName: data.orgName });
});
let cards = $derived([
{
icon: DollarSignIcon,
title: `${$t('dashboard.revenue')} ($)`,
percentage: dashAnalytics?.revenue ?? 0,
percentage: revenue ?? 0,
description: $t('dashboard.revenue_description'),
hidePercentage: true
},
{
icon: BookIcon,
title: $t('dashboard.no_of_courses'),
percentage: dashAnalytics?.numberOfCourses ?? 0,
percentage: numberOfCourses ?? 0,
description: $t('dashboard.no_courses_description'),
hidePercentage: true
},
{
icon: UsersIcon,
title: $t('dashboard.total_students'),
percentage: dashAnalytics?.totalStudents ?? 0,
percentage: totalStudents ?? 0,
description: $t('dashboard.total_students_description'),
hidePercentage: true
}
@@ -110,7 +82,7 @@
<div class="mb-10 flex flex-wrap items-start">
<div class="grid w-full grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{#each cards as card}
{#if !dashAnalytics}
{#if !revenue && !numberOfCourses && !totalStudents}
<Skeleton style="width: 100%; height: 10rem;" class="rounded-md" />
{:else}
<ActivityCard activity={card} />
@@ -123,21 +95,21 @@
<div
class="flex min-h-[45vh] w-full flex-col rounded-md border p-3 md:p-5 dark:border-neutral-600 dark:text-white"
>
<h3 class="mt-0 text-lg">
<h3 class="mb-4 text-lg">
{$t('dashboard.top_courses')}
</h3>
<div class="h-full space-y-6">
{#if !dashAnalytics}
{#if !topCourses}
{#each Array(5) as _}
<Skeleton class="h-10 w-full rounded-md" />
{/each}
{:else}
{#each dashAnalytics.topCourses as course}
{#each topCourses as course}
<div class="flex items-center gap-2">
<div class="w-4/6 space-y-1">
<a href={`/courses/${course.id}`}>
<p class="line-clamp-2 pb-[0.1rem] text-sm font-medium leading-none">
<a class="hover:underline" href={`/courses/${course.id}`}>
<p class="line-clamp-2 pb-[0.1rem] text-sm leading-none">
{course.title}
</p>
</a>
@@ -148,7 +120,7 @@
</div>
<div class="ml-auto w-2/6">
<Progress value={course.completion} />
<div class="text-sm font-medium">
<div class="text-sm">
{course.completion}%
{$t('dashboard.completion')}
</div>
@@ -171,23 +143,29 @@
<div
class="flex min-h-[45vh] w-full flex-col rounded-md border p-3 md:p-5 dark:border-neutral-600 dark:text-white"
>
<h3 class="mt-0 text-lg">
<h3 class="mb-4 text-lg">
{$t('dashboard.recent_enrollments')}
</h3>
<div class="h-full space-y-6">
{#if !dashAnalytics}
{#if !enrollments}
{#each Array(5) as _}
<Skeleton class="h-10 w-full rounded-md" />
{/each}
{:else}
{#each dashAnalytics.enrollments as enrollment}
{#each enrollments as enrollment}
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2">
<Avatar src={enrollment.avatarUrl} name={enrollment.name} width="w-6" height="h-6" />
<Avatar.Root class="h-6 w-6">
<Avatar.Image
src={enrollment.avatarUrl ? enrollment.avatarUrl : '/logo-192.png'}
alt={enrollment.name ? enrollment.name : 'User'}
/>
<Avatar.Fallback>{shortenName(enrollment.name) || 'U'}</Avatar.Fallback>
</Avatar.Root>
<div class="min-h-[45px] space-y-1">
<p class="text-sm font-medium capitalize leading-none">{enrollment.name}</p>
<p class="text-sm capitalize leading-none">{enrollment.name}</p>
<p class="text-muted-foreground text-sm">
<span class="italic">
{calDateDiff(enrollment.date)}
@@ -197,8 +175,8 @@
</div>
<div class="w-2/4">
<a href={`/courses/${enrollment.courseId}`}>
<p class="line-clamp-2 pb-[0.1rem] text-sm font-medium leading-none">
<a class="hover:underline" href={`/courses/${enrollment.courseId}`}>
<p class="line-clamp-2 pb-[0.1rem] text-sm leading-none">
{enrollment.course}
</p>
</a>