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:
co-authored by
Rotimi Best
parent
c7854efae3
commit
9a5aee1ee0
+12
-9
@@ -1,20 +1,22 @@
|
||||
import 'dotenv/config';
|
||||
import { cors } from 'hono/cors';
|
||||
import { logger } from 'hono/logger';
|
||||
import { prettyJSON } from 'hono/pretty-json';
|
||||
import { secureHeaders } from 'hono/secure-headers';
|
||||
|
||||
import { auth } from '@cio/db/auth';
|
||||
import { Hono } from '@api/utils/hono';
|
||||
import rateLimiter from '@api/middlewares/rate-limiter';
|
||||
import { API_SERVER_URL, TRUSTED_ORIGINS } from '@api/constants';
|
||||
|
||||
import { Hono } from '@api/utils/hono';
|
||||
import { accountRouter } from '@api/routes/account';
|
||||
import { auth } from '@cio/db/auth';
|
||||
import { cors } from 'hono/cors';
|
||||
import { courseRouter } from '@api/routes/course';
|
||||
import { logger } from 'hono/logger';
|
||||
// --- routes
|
||||
import { mailRouter } from '@api/routes/mail';
|
||||
import { courseRouter } from '@api/routes/course';
|
||||
import { accountRouter } from '@api/routes/account';
|
||||
import { dashAnalyticsRouter } from '@api/routes/dash';
|
||||
import { mediaRouter } from '@api/routes/media';
|
||||
import { onboardingRouter } from '@api/routes/onboarding';
|
||||
import { organizationRouter } from '@api/routes/organization';
|
||||
import { prettyJSON } from 'hono/pretty-json';
|
||||
import rateLimiter from '@api/middlewares/rate-limiter';
|
||||
import { secureHeaders } from 'hono/secure-headers';
|
||||
|
||||
// Create Hono app with chaining for RPC support
|
||||
export const app = new Hono()
|
||||
@@ -74,6 +76,7 @@ export const app = new Hono()
|
||||
.route('/mail', mailRouter)
|
||||
.route('/media', mediaRouter)
|
||||
.route('/organization', organizationRouter)
|
||||
.route('/dash', dashAnalyticsRouter)
|
||||
|
||||
// Error handling
|
||||
.onError((err, c) => {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './stats';
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Hono } from '@api/utils/hono';
|
||||
import { ZDashStats } from '@cio/utils/validation/dash';
|
||||
import { authMiddleware } from '@api/middlewares/auth';
|
||||
import { getOrganisationAnalytics } from '@api/services/dash';
|
||||
import { handleError } from '@api/utils/errors';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
|
||||
export const dashAnalyticsRouter = new Hono().post(
|
||||
'/stats',
|
||||
authMiddleware,
|
||||
zValidator('json', ZDashStats),
|
||||
async (c) => {
|
||||
try {
|
||||
const { orgId, siteName } = c.req.valid('json');
|
||||
|
||||
const result = await getOrganisationAnalytics(orgId, siteName);
|
||||
|
||||
return c.json({ success: true, data: result }, 201);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to load organisation analytics');
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,60 @@
|
||||
import { AppError, ErrorCodes } from '@api/utils/errors';
|
||||
import { getCourseStats, getDashOrgStats, getRecentEnrollments } from '@cio/db/queries/dash';
|
||||
|
||||
import { OrganisationAnalytics } from '@api/types';
|
||||
import { getOrgIdBySiteName } from '@db/queries';
|
||||
|
||||
export async function getOrganisationAnalytics(orgId?: string, siteName?: string): Promise<OrganisationAnalytics> {
|
||||
const analytics: OrganisationAnalytics = {
|
||||
revenue: 0,
|
||||
numberOfCourses: 0,
|
||||
totalStudents: 0,
|
||||
topCourses: [],
|
||||
enrollments: []
|
||||
};
|
||||
|
||||
let resolvedOrgId = orgId;
|
||||
|
||||
if (!resolvedOrgId && siteName) {
|
||||
const [org] = await getOrgIdBySiteName(siteName);
|
||||
|
||||
if (org) {
|
||||
resolvedOrgId = org.id;
|
||||
} else {
|
||||
throw new AppError('Organization not found for the given site name', ErrorCodes.ORG_NOT_FOUND, 404);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const [stats, topCourses, enrollments] = await Promise.all([
|
||||
getDashOrgStats(resolvedOrgId!),
|
||||
getCourseStats(resolvedOrgId!),
|
||||
getRecentEnrollments(resolvedOrgId!)
|
||||
]);
|
||||
|
||||
analytics.revenue = 0;
|
||||
analytics.numberOfCourses = stats?.[0]?.noOfCourses ?? 0;
|
||||
analytics.totalStudents = stats?.[0]?.enrolledStudents ?? 0;
|
||||
|
||||
analytics.topCourses = topCourses.map((c) => ({
|
||||
id: c.courseId,
|
||||
title: c.courseTitle,
|
||||
enrollments: c.totalStudents,
|
||||
completion: c.completionPercentage
|
||||
}));
|
||||
|
||||
analytics.enrollments = enrollments.map((e) => ({
|
||||
id: e.profileId,
|
||||
avatarUrl: e.avatarUrl,
|
||||
name: e.fullname,
|
||||
courseId: e.courseId,
|
||||
course: e.courseTitle,
|
||||
date: e.enrolledAt
|
||||
}));
|
||||
|
||||
return analytics;
|
||||
} catch (error) {
|
||||
console.error('Failed to load organisation analytics:', error);
|
||||
throw new AppError('Failed to load organisation analytics', ErrorCodes.ORG_ANALYTICS_FETCH_FAILED, 500);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './account';
|
||||
export * from './media';
|
||||
export * from './onboarding';
|
||||
export * from './dash';
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
export interface TopCourse {
|
||||
id: string;
|
||||
title: string;
|
||||
enrollments: number;
|
||||
completion: number;
|
||||
}
|
||||
|
||||
export interface Enrollment {
|
||||
id: string;
|
||||
avatarUrl: string | null;
|
||||
name: string;
|
||||
courseId: string;
|
||||
course: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export interface OrganisationAnalytics {
|
||||
revenue: number;
|
||||
numberOfCourses: number;
|
||||
totalStudents: number;
|
||||
topCourses: TopCourse[];
|
||||
enrollments: Enrollment[];
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
// Re-export all types
|
||||
export * from './database';
|
||||
export * from './dash';
|
||||
|
||||
@@ -28,6 +28,10 @@ export interface ErrorResponse {
|
||||
|
||||
// Error codes constants for consistency
|
||||
export const ErrorCodes = {
|
||||
// Dashboard Analytics errors
|
||||
ORG_ANALYTICS_FETCH_FAILED: 'ORG_ANALYTICS_FETCH_FAILED',
|
||||
NO_ORG_ID_PROVIDED: 'NO_ORG_ID_PROVIDED',
|
||||
|
||||
// Organization errors
|
||||
SITENAME_EXISTS: 'SITENAME_EXISTS',
|
||||
ORG_CREATE_FAILED: 'ORG_CREATE_FAILED',
|
||||
|
||||
Reference in New Issue
Block a user