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 <best@classroomio.com>
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,8 +10,9 @@
import { Empty } from '@cio/ui/custom/empty';
import { UpgradeBanner } from '$lib/features/ui';
import { currentOrg, currentOrgMaxAudience } from '$lib/utils/store/org';
import Avatar from '$lib/components/Avatar/index.svelte';
import * as Avatar from '@cio/ui/base/avatar';
import * as Page from '@cio/ui/base/page';
import { shortenName } from '$lib/utils/functions/string';
$effect(() => {
orgApi.getOrgAudience($currentOrg.id);
@@ -81,7 +82,13 @@
href={`${pageStore.url.href}/${row.id}/${$currentOrg.id}`}
class="text-primary-700 flex items-center gap-2 hover:underline"
>
<Avatar src={row.avatar_url} width="w-5" height="h-5" />
<Avatar.Root class="h-5 w-5">
<Avatar.Image
src={row.avatar_url ? row.avatar_url : '/logo-192.png'}
alt={row.name ? row.name : 'User'}
/>
<Avatar.Fallback>{shortenName(row.name) || 'U'}</Avatar.Fallback>
</Avatar.Root>
{row.name}
</a>
</Table.Cell>
@@ -0,0 +1,34 @@
import { BaseApi, classroomio } from '$lib/utils/services/api';
import type { DashStatsSuccess } from '$lib/features/org/utils/types';
/**
* API class for organization dashboard analytics
*/
class DashStatApi extends BaseApi {
stats = $state<DashStatsSuccess['data']>({
enrollments: [],
numberOfCourses: 0,
revenue: 0,
totalStudents: 0,
topCourses: []
});
/**
* Fetches dashboard statistics for an organization
* @param orgId Optional organization ID
* @param siteName Optional site name
* @returns Organization analytics data (enrollments, courses, revenue, students, top courses)
*/
async fetchOrgStats({ orgId, siteName }: { orgId?: string; siteName?: string }) {
await this.execute<typeof classroomio.dash.stats.$post>({
requestFn: () => classroomio.dash.stats.$post({ json: { orgId, siteName } }),
logContext: 'fetching organization stats',
onSuccess: (response) => {
this.stats = response.data;
}
});
}
}
export const dashStatApi = new DashStatApi();
@@ -1,2 +1,3 @@
export * from './org.svelte';
export * from './org-plan.svelte';
export * from './dash-stats.svelte';
@@ -33,6 +33,11 @@ export type CoursesByOrganizationSiteNameSuccess = Extract<
export type CoursesByOrganizationSiteName = CoursesByOrganizationSiteNameSuccess['data'];
// dashboard analytics types
export type GetDashStatsResponse = typeof classroomio.dash.stats.$post;
export type DashStatsResponse = InferResponseType<GetDashStatsResponse> | null;
export type DashStatsSuccess = Extract<InferResponseType<GetDashStatsResponse>, { success: true }>;
export type InviteTeamResponse = (typeof classroomio.organization)[':orgId']['team']['invite']['$post'];
export type InviteTeamSuccess = Extract<InferResponseType<InviteTeamResponse>, { success: true }>;
export type InviteTeamData = InviteTeamSuccess['data'];
@@ -47,7 +47,7 @@
</svg>
<!-- Center text -->
<div class="absolute inset-0 flex flex-col items-center justify-center">
<p class="{size === 'small' ? 'text-[10px]' : 'text-sm'} font-bold">{progressPercentage}%</p>
<p class={size === 'small' ? 'text-[10px]' : 'text-sm'}>{progressPercentage}%</p>
</div>
{/if}
</div>
@@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/state';
import Avatar from '$lib/components/Avatar/index.svelte';
import * as Avatar from '@cio/ui/base/avatar';
import { t } from '$lib/utils/functions/translations';
import { currentOrg } from '$lib/utils/store/org';
import GoogleIconColored from '$lib/components/Icons/GoogleIconColored.svelte';
@@ -68,14 +68,14 @@
<Card.Root class="ui:w-full max-w-[400px]">
{#if !showOnlyContent || showLogo}
<Card.Header class="ui:flex ui:flex-col ui:items-center ui:gap-4">
<Avatar
src={$currentOrg.avatarUrl ? $currentOrg.avatarUrl : '/logo-192.png'}
name={$currentOrg.name ? $currentOrg.name : 'ClassroomIO'}
shape="rounded-md"
width="w-10"
height="max-h-10"
className="mr-2"
/>
<Avatar.Root>
<Avatar.Image
src={$currentOrg.avatarUrl ? $currentOrg.avatarUrl : '/logo-192.png'}
alt={$currentOrg.name ? $currentOrg.name : 'ClassroomIO'}
/>
<Avatar.Fallback>{$currentOrg.name ? $currentOrg.name : 'ClassroomIO'}</Avatar.Fallback>
</Avatar.Root>
<a href="/">
<Card.Title class="ui:text-2xl ui:font-bold">
{isLogin ? $t('login.welcome') : $t('login.create_account')}
@@ -8,7 +8,7 @@
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
import { getShortOrgName } from '$lib/utils/functions/org';
import { shortenName } from '$lib/utils/functions/string';
import type { AccountOrg } from '$lib/features/app/types';
import { setTheme } from '$lib/utils/functions/theme';
@@ -44,7 +44,7 @@
{#if $currentOrg.name}
<Avatar.Root class="rounded-md! size-6! flex items-center justify-center">
<Avatar.Image src={$currentOrg.avatarUrl} alt={$currentOrg.name} />
<Avatar.Fallback class="rounded-md! text-xs">{getShortOrgName($currentOrg.name)}</Avatar.Fallback>
<Avatar.Fallback class="rounded-md! text-xs">{shortenName($currentOrg.name)}</Avatar.Fallback>
</Avatar.Root>
<span class="hidden truncate text-sm font-medium md:block">{$currentOrg.name}</span>
<ChevronDownIcon class="ml-auto hidden size-4 md:block" />
@@ -66,7 +66,7 @@
<DropdownMenu.Item onSelect={() => onClick(org)} class="gap-2 p-2">
<Avatar.Root class="flex size-8 items-center justify-center rounded-lg">
<Avatar.Image src={org.avatarUrl} alt={org.name} />
<Avatar.Fallback class="rounded-lg">{getShortOrgName(org.name)}</Avatar.Fallback>
<Avatar.Fallback class="rounded-lg">{shortenName(org.name)}</Avatar.Fallback>
</Avatar.Root>
{org.name}
@@ -100,7 +100,7 @@
{#if $currentOrg.name}
<Avatar.Root class="ui:flex ui:size-8 ui:items-center ui:justify-center ui:rounded-lg">
<Avatar.Image src={$currentOrg.avatarUrl} alt={$currentOrg.name} />
<Avatar.Fallback class="rounded-lg">{getShortOrgName($currentOrg.name)}</Avatar.Fallback>
<Avatar.Fallback class="rounded-lg">{shortenName($currentOrg.name)}</Avatar.Fallback>
</Avatar.Root>
<div class="grid flex-1 text-left text-sm leading-tight">
@@ -132,7 +132,7 @@
<DropdownMenu.Item onSelect={() => onClick(org)} class="gap-2 p-2">
<Avatar.Root class="flex size-8 items-center justify-center rounded-lg">
<Avatar.Image src={org.avatarUrl} alt={org.name} />
<Avatar.Fallback class="rounded-lg">{getShortOrgName(org.name)}</Avatar.Fallback>
<Avatar.Fallback class="rounded-lg">{shortenName(org.name)}</Avatar.Fallback>
</Avatar.Root>
{org.name}