diff --git a/components/__tests__/all-time-perlearner-box.test.tsx b/components/__tests__/all-time-perlearner-box.test.tsx
index 6f636eb..3d8e896 100644
--- a/components/__tests__/all-time-perlearner-box.test.tsx
+++ b/components/__tests__/all-time-perlearner-box.test.tsx
@@ -1,14 +1,23 @@
-import { describe, it, expect } from 'vitest';
+import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
+
+vi.mock('next/navigation', () => ({
+ useParams: () => ({ tenant: 'test-tenant' }),
+}));
+
+vi.mock('@/utils/helpers', () => ({
+ getTenant: vi.fn(() => 'test-tenant'),
+}));
+
import { AllTimePerLearnerBox } from '../all-time-perlearner-box';
describe('AllTimePerLearnerBox', () => {
const defaultProps = {
- total_assessments: 10,
total_time_spent: 7200,
- total_videos: 5,
- course_completions: 3,
+ courses: 4,
+ credentials: 2,
+ skills: 6,
};
it('renders without crashing', () => {
@@ -16,9 +25,9 @@ describe('AllTimePerLearnerBox', () => {
expect(container.firstChild).toBeInTheDocument();
});
- it('renders "All Time" heading', () => {
+ it('renders "Highlights" heading', () => {
render();
- expect(screen.getByText('All Time')).toBeInTheDocument();
+ expect(screen.getByText('Highlights')).toBeInTheDocument();
});
it('renders Time Spent label', () => {
@@ -26,19 +35,19 @@ describe('AllTimePerLearnerBox', () => {
expect(screen.getByText('Time Spent')).toBeInTheDocument();
});
- it('renders Watched Video label', () => {
+ it('renders Courses label', () => {
render();
- expect(screen.getByText('Watched Video')).toBeInTheDocument();
+ expect(screen.getByText('Courses')).toBeInTheDocument();
});
- it('renders Assessments label', () => {
+ it('renders Credentials label', () => {
render();
- expect(screen.getByText('Assessments')).toBeInTheDocument();
+ expect(screen.getByText('Credentials')).toBeInTheDocument();
});
- it('renders Courses Completions label', () => {
+ it('renders Skills label', () => {
render();
- expect(screen.getByText('Courses Completions')).toBeInTheDocument();
+ expect(screen.getByText('Skills')).toBeInTheDocument();
});
it('converts time spent to hours correctly', () => {
@@ -47,19 +56,25 @@ describe('AllTimePerLearnerBox', () => {
expect(screen.getByText('2 hours')).toBeInTheDocument();
});
- it('displays total videos count', () => {
+ it('displays courses count as a link to the profile courses page', () => {
render();
- expect(screen.getByText('5')).toBeInTheDocument();
+ const link = screen.getByRole('link', { name: 'View courses' });
+ expect(link).toHaveTextContent('4');
+ expect(link).toHaveAttribute('href', '/platform/test-tenant/profile/courses');
});
- it('displays total assessments count', () => {
+ it('displays credentials count as a link to the profile credentials page', () => {
render();
- expect(screen.getByText('10')).toBeInTheDocument();
+ const link = screen.getByRole('link', { name: 'View credentials' });
+ expect(link).toHaveTextContent('2');
+ expect(link).toHaveAttribute('href', '/platform/test-tenant/profile/credentials');
});
- it('displays course completions count', () => {
+ it('displays skills count as a link to the profile skills page', () => {
render();
- expect(screen.getByText('3')).toBeInTheDocument();
+ const link = screen.getByRole('link', { name: 'View skills' });
+ expect(link).toHaveTextContent('6');
+ expect(link).toHaveAttribute('href', '/platform/test-tenant/profile/skills');
});
it('handles zero time spent', () => {
@@ -84,14 +99,7 @@ describe('AllTimePerLearnerBox', () => {
});
it('handles zero values for all props', () => {
- render(
- ,
- );
+ render();
expect(screen.getByText('0 hours')).toBeInTheDocument();
// All zeros displayed
const zeros = screen.getAllByText('0');
diff --git a/components/__tests__/credentials-list-box.test.tsx b/components/__tests__/credentials-list-box.test.tsx
index 7b56e55..02b8a20 100644
--- a/components/__tests__/credentials-list-box.test.tsx
+++ b/components/__tests__/credentials-list-box.test.tsx
@@ -89,11 +89,9 @@ describe('CredentialsListBox', () => {
expect(screen.getByText('Credential B')).toBeInTheDocument();
});
- it('shows empty message when no credentials', () => {
+ it('does not show an empty message when no credentials', () => {
render();
- // DefaultEmptyBox is rendered with message "No credentials yet."
- // Since we're not mocking DefaultEmptyBox, it renders the actual component
- expect(screen.getByText('No credentials yet.')).toBeInTheDocument();
+ expect(screen.queryByText('No credentials yet.')).not.toBeInTheDocument();
});
it('renders correct number of credential items', () => {
diff --git a/components/__tests__/profile-sidebar.test.tsx b/components/__tests__/profile-sidebar.test.tsx
index e65b978..eb4ca05 100644
--- a/components/__tests__/profile-sidebar.test.tsx
+++ b/components/__tests__/profile-sidebar.test.tsx
@@ -24,7 +24,9 @@ vi.mock('../credentials-list-box', () => ({
vi.mock('../all-time-perlearner-box', () => ({
AllTimePerLearnerBox: (props: any) => (
-
All Time: {props.total_time_spent}
+
+ All Time: {props.total_time_spent} / {props.courses} / {props.credentials} / {props.skills}
+
),
}));
@@ -56,6 +58,12 @@ const mockUseUserMetadata = vi.fn(() => ({
userMetaDataLoading: false,
}));
+const mockUseAllTimeStats = vi.fn(() => ({
+ courses: 4,
+ credentials: 2,
+ skills: 6,
+}));
+
vi.mock('@/hooks/skills/use-latest-skills', () => ({
useLatestSkills: () => mockUseLatestSkills(),
}));
@@ -72,6 +80,10 @@ vi.mock('@/hooks/users/use-usermetadata', () => ({
useUserMetadata: () => mockUseUserMetadata(),
}));
+vi.mock('@/hooks/profile/use-all-time-stats', () => ({
+ useAllTimeStats: () => mockUseAllTimeStats(),
+}));
+
import { ProfileSidebar } from '../profile-sidebar';
describe('ProfileSidebar', () => {
@@ -95,6 +107,11 @@ describe('ProfileSidebar', () => {
},
userPerLearnerInfoLoading: false,
});
+ mockUseAllTimeStats.mockReturnValue({
+ courses: 4,
+ credentials: 2,
+ skills: 6,
+ });
});
it('renders without crashing', () => {
@@ -143,15 +160,15 @@ describe('ProfileSidebar', () => {
it('passes correct props to AllTimePerLearnerBox', () => {
render();
- expect(screen.getByText('All Time: 3600')).toBeInTheDocument();
+ expect(screen.getByText('All Time: 3600 / 4 / 2 / 6')).toBeInTheDocument();
});
- it('defaults to 0 when perlearner info values are undefined', () => {
+ it('defaults time spent to 0 when perlearner info values are undefined', () => {
mockUsePerLearnerInfoQuery.mockReturnValue({
userPerLearnerInfo: undefined,
userPerLearnerInfoLoading: false,
} as any);
render();
- expect(screen.getByText('All Time: 0')).toBeInTheDocument();
+ expect(screen.getByText('All Time: 0 / 4 / 2 / 6')).toBeInTheDocument();
});
});
diff --git a/components/all-time-perlearner-box.tsx b/components/all-time-perlearner-box.tsx
index 8756c87..8f0c3f8 100644
--- a/components/all-time-perlearner-box.tsx
+++ b/components/all-time-perlearner-box.tsx
@@ -1,18 +1,22 @@
-import { Award, Clock, FileText, Play } from 'lucide-react';
+'use client';
+import { Award, BookOpen, Clock, Sparkles } from 'lucide-react';
+import Link from 'next/link';
+import { useTenantParam } from '@/hooks/use-tenant-param';
interface AllTimePerLearnerBoxProps {
- total_assessments: number;
total_time_spent: number;
- total_videos: number;
- course_completions: number;
+ courses: number;
+ credentials: number;
+ skills: number;
}
export const AllTimePerLearnerBox = ({
- total_assessments,
total_time_spent,
- total_videos,
- course_completions,
+ courses,
+ credentials,
+ skills,
}: AllTimePerLearnerBoxProps) => {
+ const tenant = useTenantParam();
const hours =
typeof total_time_spent === 'number' && !isNaN(total_time_spent)
? Math.round(total_time_spent / 3600)
@@ -20,7 +24,9 @@ export const AllTimePerLearnerBox = ({
return (
-
All Time
+
+ Highlights
+
@@ -34,29 +40,47 @@ export const AllTimePerLearnerBox = ({
-
Watched Video
+
Courses
-
{total_videos}
+
+ {courses}
+
-
Assessments
+
Credentials
-
{total_assessments}
+
+ {credentials}
+
-
Courses Completions
+
Skills
-
{course_completions}
+
+ {skills}
+
diff --git a/components/credentials-list-box.tsx b/components/credentials-list-box.tsx
index 956b983..5760041 100644
--- a/components/credentials-list-box.tsx
+++ b/components/credentials-list-box.tsx
@@ -2,7 +2,6 @@
import { Plus } from 'lucide-react';
import { Assertion } from '@iblai/iblai-api';
import { CredentialMiniBox } from './credential-mini-box';
-import { DefaultEmptyBox } from './default-empty-box';
import Link from 'next/link';
import { useTenantParam } from '@/hooks/use-tenant-param';
type CredentialsListBoxProps = {
@@ -27,9 +26,6 @@ export function CredentialsListBox({ credentials }: CredentialsListBoxProps) {
- {credentials.length === 0 && (
-
- )}
{credentials.map((credential, index) => (
) : (
)}
diff --git a/hooks/profile/__tests__/use-all-time-stats.test.ts b/hooks/profile/__tests__/use-all-time-stats.test.ts
new file mode 100644
index 0000000..634933a
--- /dev/null
+++ b/hooks/profile/__tests__/use-all-time-stats.test.ts
@@ -0,0 +1,112 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { renderHook, waitFor } from '@testing-library/react';
+import '@testing-library/jest-dom';
+
+vi.mock('@/utils/helpers', () => ({
+ getTenant: vi.fn(() => 'test-tenant'),
+ getUserId: vi.fn(() => 42),
+ getUserName: vi.fn(() => 'test-user'),
+}));
+
+const mockGetUserReportedSkills = vi.fn();
+const mockGetUserDesiredSkills = vi.fn();
+
+vi.mock('@iblai/iblai-js/data-layer', () => ({
+ useLazyGetUserReportedSkillsQuery: vi.fn(() => [mockGetUserReportedSkills, { isError: false }]),
+ useLazyGetUserDesiredSkillsQuery: vi.fn(() => [mockGetUserDesiredSkills, { isError: false }]),
+}));
+
+const mockGetUserCredentials = vi.fn();
+vi.mock('@/services/credentials', () => ({
+ useLazyGetUserCredentialsQuery: vi.fn(() => [mockGetUserCredentials, { isError: false }]),
+}));
+
+const mockGetUserEnrolledCourses = vi.fn();
+vi.mock('@/services/courses', () => ({
+ useLazyGetUserEnrolledCoursesQuery: vi.fn(() => [mockGetUserEnrolledCourses, { isError: false }]),
+}));
+
+import { useAllTimeStats } from '../use-all-time-stats';
+
+describe('useAllTimeStats', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockGetUserReportedSkills.mockResolvedValue({ data: null });
+ mockGetUserDesiredSkills.mockResolvedValue({ data: null });
+ mockGetUserCredentials.mockResolvedValue({ data: null });
+ mockGetUserEnrolledCourses.mockResolvedValue({ data: null });
+ });
+
+ it('returns expected shape with zero defaults', () => {
+ const { result } = renderHook(() => useAllTimeStats());
+ expect(result.current.courses).toBe(0);
+ expect(result.current.credentials).toBe(0);
+ expect(result.current.skills).toBe(0);
+ });
+
+ it('updates skills count from reported and desired skills', async () => {
+ mockGetUserReportedSkills.mockResolvedValue({
+ data: { skills: [{ name: 'Python' }, { name: 'JavaScript' }] },
+ });
+ mockGetUserDesiredSkills.mockResolvedValue({
+ data: { skills: [{ name: 'React' }] },
+ });
+
+ const { result } = renderHook(() => useAllTimeStats());
+
+ await waitFor(() => {
+ expect(result.current.skills).toBe(3);
+ });
+ });
+
+ it('updates credentials count', async () => {
+ mockGetUserCredentials.mockResolvedValue({
+ data: { data: [{ id: 'cred-1' }, { id: 'cred-2' }] },
+ });
+
+ const { result } = renderHook(() => useAllTimeStats());
+
+ await waitFor(() => {
+ expect(result.current.credentials).toBe(2);
+ });
+ });
+
+ it('updates courses count', async () => {
+ mockGetUserEnrolledCourses.mockResolvedValue({
+ data: { count: 5 },
+ });
+
+ const { result } = renderHook(() => useAllTimeStats());
+
+ await waitFor(() => {
+ expect(result.current.courses).toBe(5);
+ });
+ });
+
+ it('keeps counts at 0 on empty responses', async () => {
+ const { result } = renderHook(() => useAllTimeStats());
+
+ await waitFor(() => {
+ expect(mockGetUserEnrolledCourses).toHaveBeenCalled();
+ });
+ expect(result.current.courses).toBe(0);
+ expect(result.current.credentials).toBe(0);
+ expect(result.current.skills).toBe(0);
+ });
+
+ it('keeps counts at 0 when fetches reject', async () => {
+ mockGetUserReportedSkills.mockRejectedValue(new Error('network'));
+ mockGetUserDesiredSkills.mockRejectedValue(new Error('network'));
+ mockGetUserCredentials.mockRejectedValue(new Error('network'));
+ mockGetUserEnrolledCourses.mockRejectedValue(new Error('network'));
+
+ const { result } = renderHook(() => useAllTimeStats());
+
+ await waitFor(() => {
+ expect(mockGetUserEnrolledCourses).toHaveBeenCalled();
+ });
+ expect(result.current.courses).toBe(0);
+ expect(result.current.credentials).toBe(0);
+ expect(result.current.skills).toBe(0);
+ });
+});
diff --git a/hooks/profile/use-all-time-stats.ts b/hooks/profile/use-all-time-stats.ts
new file mode 100644
index 0000000..4b9a507
--- /dev/null
+++ b/hooks/profile/use-all-time-stats.ts
@@ -0,0 +1,109 @@
+import { getTenant, getUserId, getUserName } from '@/utils/helpers';
+import { useState, useEffect } from 'react';
+import {
+ // @ts-ignore
+ useLazyGetUserReportedSkillsQuery,
+ // @ts-ignore
+ useLazyGetUserDesiredSkillsQuery,
+} from '@iblai/iblai-js/data-layer';
+import _ from 'lodash';
+import { useLazyGetUserCredentialsQuery } from '@/services/credentials';
+import { useLazyGetUserEnrolledCoursesQuery } from '@/services/courses';
+
+export const useAllTimeStats = () => {
+ const [getUserReportedSkills, { isError: isErrorGetUserReportedSkills }] =
+ useLazyGetUserReportedSkillsQuery();
+ const [getUserDesiredSkills, { isError: isErrorGetUserDesiredSkills }] =
+ useLazyGetUserDesiredSkillsQuery();
+ const [getUserCredentials, { isError: isErrorGetUserCredentials }] =
+ useLazyGetUserCredentialsQuery();
+ const [getUserEnrolledCourses, { isError: isErrorGetUserEnrolledCourses }] =
+ useLazyGetUserEnrolledCoursesQuery();
+
+ const [courses, setCourses] = useState(0);
+ const [credentials, setCredentials] = useState(0);
+ const [skills, setSkills] = useState(0);
+
+ const handleSkillsStats = async () => {
+ try {
+ const reportedSkills = await getUserReportedSkills(
+ [
+ {
+ userId: getUserId(),
+ username: getUserName(),
+ },
+ ],
+ true,
+ );
+ let skillsCount = 0;
+ if (isErrorGetUserDesiredSkills && isErrorGetUserReportedSkills) {
+ throw new Error();
+ }
+ if (!isErrorGetUserReportedSkills) {
+ skillsCount = reportedSkills?.data?.skills?.length || 0;
+ }
+ const earnedSkills = await getUserDesiredSkills(
+ [
+ {
+ userId: getUserId(),
+ username: getUserName(),
+ },
+ ],
+ true,
+ );
+ if (!isErrorGetUserDesiredSkills) {
+ skillsCount += earnedSkills?.data?.skills?.length || 0;
+ }
+ setSkills(skillsCount);
+ } catch {
+ setSkills(0);
+ }
+ };
+
+ const handleCredentialsStats = async () => {
+ try {
+ const response = await getUserCredentials(
+ {
+ org: getTenant(),
+ username: getUserName(),
+ },
+ true,
+ );
+ if (isErrorGetUserCredentials || _.isEmpty(response?.data)) {
+ throw new Error();
+ }
+ setCredentials(response?.data?.data?.length || 0);
+ } catch {
+ setCredentials(0);
+ }
+ };
+
+ const handleCoursesStats = async () => {
+ try {
+ const response = await getUserEnrolledCourses(
+ {
+ username: getUserName(),
+ },
+ true,
+ );
+ if (isErrorGetUserEnrolledCourses || _.isEmpty(response.data)) {
+ throw new Error();
+ }
+ setCourses(response?.data?.count || 0);
+ } catch {
+ setCourses(0);
+ }
+ };
+
+ useEffect(() => {
+ handleSkillsStats();
+ handleCredentialsStats();
+ handleCoursesStats();
+ }, []);
+
+ return {
+ courses,
+ credentials,
+ skills,
+ };
+};