CognxSafeTrack commited on
Commit
6c2d132
·
1 Parent(s): 6faac59

feat: implement dynamic admin dashboard with API data

Browse files
apps/admin/src/App.tsx CHANGED
@@ -1,21 +1,115 @@
1
  import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
2
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  function Dashboard() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  return (
5
  <div className="p-8">
6
- <h1 className="text-3xl font-bold mb-4">Admin Dashboard</h1>
7
- <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
8
- <div className="bg-white p-6 rounded-lg shadow-sm border">
9
- <h2 className="text-xl font-semibold mb-2">Users</h2>
10
- <p className="text-gray-600">Manage student enrollments</p>
 
 
 
 
 
 
11
  </div>
12
- <div className="bg-white p-6 rounded-lg shadow-sm border">
13
- <h2 className="text-xl font-semibold mb-2">Tracks</h2>
14
- <p className="text-gray-600">Edit learning content</p>
 
 
 
 
 
 
 
 
 
 
 
15
  </div>
16
- <div className="bg-white p-6 rounded-lg shadow-sm border">
17
- <h2 className="text-xl font-semibold mb-2">Analytics</h2>
18
- <p className="text-gray-600">View platform metrics</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </div>
20
  </div>
21
  </div>
 
1
  import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
2
 
3
+ import { useEffect, useState } from 'react';
4
+
5
+ interface DashboardData {
6
+ stats: {
7
+ totalUsers: number;
8
+ activeEnrollments: number;
9
+ completedEnrollments: number;
10
+ totalTracks: number;
11
+ } | null;
12
+ enrollments: any[];
13
+ }
14
+
15
  function Dashboard() {
16
+ const [data, setData] = useState<DashboardData>({ stats: null, enrollments: [] });
17
+ const [loading, setLoading] = useState(true);
18
+
19
+ useEffect(() => {
20
+ const fetchData = async () => {
21
+ try {
22
+ const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001';
23
+
24
+ const [statsRes, enrollmentsRes] = await Promise.all([
25
+ fetch(`${API_URL}/v1/admin/stats`),
26
+ fetch(`${API_URL}/v1/admin/enrollments`)
27
+ ]);
28
+
29
+ const stats = await statsRes.json();
30
+ const enrollments = await enrollmentsRes.json();
31
+
32
+ setData({ stats, enrollments });
33
+ } catch (error) {
34
+ console.error("Error fetching dashboard data", error);
35
+ } finally {
36
+ setLoading(false);
37
+ }
38
+ };
39
+
40
+ fetchData();
41
+ }, []);
42
+
43
+ if (loading) return <div className="p-8">Loading dashboard...</div>;
44
+
45
  return (
46
  <div className="p-8">
47
+ <h1 className="text-3xl font-bold mb-8 text-slate-800">Admin Dashboard</h1>
48
+
49
+ {/* Stats Overview */}
50
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
51
+ <div className="bg-white p-6 rounded-xl shadow-sm border border-slate-100 flex flex-col justify-center items-center">
52
+ <h2 className="text-sm font-semibold text-slate-500 uppercase tracking-wider mb-1">Total Users</h2>
53
+ <p className="text-4xl font-bold text-slate-900">{data.stats?.totalUsers || 0}</p>
54
+ </div>
55
+ <div className="bg-white p-6 rounded-xl shadow-sm border border-slate-100 flex flex-col justify-center items-center">
56
+ <h2 className="text-sm font-semibold text-slate-500 uppercase tracking-wider mb-1">Active Enrollments</h2>
57
+ <p className="text-4xl font-bold text-blue-600">{data.stats?.activeEnrollments || 0}</p>
58
  </div>
59
+ <div className="bg-white p-6 rounded-xl shadow-sm border border-slate-100 flex flex-col justify-center items-center">
60
+ <h2 className="text-sm font-semibold text-slate-500 uppercase tracking-wider mb-1">Completed</h2>
61
+ <p className="text-4xl font-bold text-green-600">{data.stats?.completedEnrollments || 0}</p>
62
+ </div>
63
+ <div className="bg-white p-6 rounded-xl shadow-sm border border-slate-100 flex flex-col justify-center items-center">
64
+ <h2 className="text-sm font-semibold text-slate-500 uppercase tracking-wider mb-1">Tracks</h2>
65
+ <p className="text-4xl font-bold text-purple-600">{data.stats?.totalTracks || 0}</p>
66
+ </div>
67
+ </div>
68
+
69
+ {/* Recent Enrollments Table */}
70
+ <div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
71
+ <div className="px-6 py-4 border-b border-slate-100">
72
+ <h2 className="text-lg font-semibold text-slate-800">Recent Enrollments</h2>
73
  </div>
74
+ <div className="overflow-x-auto">
75
+ <table className="w-full text-sm text-left">
76
+ <thead className="text-xs text-slate-500 bg-slate-50 uppercase">
77
+ <tr>
78
+ <th className="px-6 py-3">User (WhatsApp)</th>
79
+ <th className="px-6 py-3">Track</th>
80
+ <th className="px-6 py-3">Status</th>
81
+ <th className="px-6 py-3">Progress</th>
82
+ <th className="px-6 py-3">Date</th>
83
+ </tr>
84
+ </thead>
85
+ <tbody>
86
+ {data.enrollments.map((env: any) => (
87
+ <tr key={env.id} className="border-b border-slate-50 hover:bg-slate-50/50">
88
+ <td className="px-6 py-4 font-medium text-slate-900">{env.user?.whatsappId || 'Unknown'}</td>
89
+ <td className="px-6 py-4">{env.track?.title || 'Unknown Track'}</td>
90
+ <td className="px-6 py-4">
91
+ <span className={`px-2.5 py-1 py-0.5 rounded-full text-xs font-medium
92
+ ${env.status === 'ACTIVE' ? 'bg-blue-100 text-blue-800' :
93
+ env.status === 'COMPLETED' ? 'bg-green-100 text-green-800' :
94
+ 'bg-slate-100 text-slate-800'}`}>
95
+ {env.status}
96
+ </span>
97
+ </td>
98
+ <td className="px-6 py-4">Day {env.currentDay}</td>
99
+ <td className="px-6 py-4 text-slate-500">
100
+ {new Date(env.startedAt).toLocaleDateString()}
101
+ </td>
102
+ </tr>
103
+ ))}
104
+ {data.enrollments.length === 0 && (
105
+ <tr>
106
+ <td colSpan={5} className="px-6 py-8 text-center text-slate-500">
107
+ No enrollments found.
108
+ </td>
109
+ </tr>
110
+ )}
111
+ </tbody>
112
+ </table>
113
  </div>
114
  </div>
115
  </div>
apps/admin/src/vite-env.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ /// <reference types="vite/client" />
apps/api/src/index.ts CHANGED
@@ -11,6 +11,9 @@ server.register(cors);
11
  import { whatsappRoutes } from './routes/whatsapp';
12
  server.register(whatsappRoutes, { prefix: '/v1/whatsapp' });
13
 
 
 
 
14
  server.get('/health', async () => {
15
  return { status: 'ok', timestamp: new Date().toISOString() };
16
  });
 
11
  import { whatsappRoutes } from './routes/whatsapp';
12
  server.register(whatsappRoutes, { prefix: '/v1/whatsapp' });
13
 
14
+ import { adminRoutes } from './routes/admin';
15
+ server.register(adminRoutes, { prefix: '/v1/admin' });
16
+
17
  server.get('/health', async () => {
18
  return { status: 'ok', timestamp: new Date().toISOString() };
19
  });
apps/api/src/routes/admin.ts ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FastifyInstance } from 'fastify';
2
+ import { PrismaClient } from '@prisma/client';
3
+
4
+ const prisma = new PrismaClient();
5
+
6
+ export async function adminRoutes(fastify: FastifyInstance) {
7
+ // 1. Get Dashboard Stats
8
+ fastify.get('/stats', async () => {
9
+ const totalUsers = await prisma.user.count();
10
+ const activeEnrollments = await prisma.enrollment.count({ where: { status: 'ACTIVE' } });
11
+ const completedEnrollments = await prisma.enrollment.count({ where: { status: 'COMPLETED' } });
12
+ const totalTracks = await prisma.track.count();
13
+
14
+ return {
15
+ totalUsers,
16
+ activeEnrollments,
17
+ completedEnrollments,
18
+ totalTracks
19
+ };
20
+ });
21
+
22
+ // 2. Get Users List
23
+ fastify.get('/users', async () => {
24
+ const users = await prisma.user.findMany({
25
+ orderBy: { createdAt: 'desc' },
26
+ take: 50,
27
+ });
28
+ return users;
29
+ });
30
+
31
+ // 3. Get Recent Enrollments
32
+ fastify.get('/enrollments', async () => {
33
+ const enrollments = await prisma.enrollment.findMany({
34
+ include: {
35
+ user: true,
36
+ track: true,
37
+ },
38
+ orderBy: { startedAt: 'desc' },
39
+ take: 50,
40
+ });
41
+ return enrollments;
42
+ });
43
+ }