From 69cc3e75023df2b459ad8c72064b329a41af893a Mon Sep 17 00:00:00 2001 From: KVSNIKHIL1998 Date: Sun, 17 May 2026 02:38:23 +1000 Subject: [PATCH 1/2] feat(dashboard) --- src/modules/dashboard/dashboard.controller.ts | 72 ++++++++++++ src/modules/dashboard/dashboard.routes.ts | 13 ++ src/modules/dashboard/dashboard.service.ts | 111 ++++++++++++++++++ src/routes/index.ts | 4 + tests/integration/dashboard-widgets.test.ts | 43 ++++++- tests/unit/dashboard-widgets.test.ts | 21 +++- 6 files changed, 258 insertions(+), 6 deletions(-) create mode 100644 src/modules/dashboard/dashboard.controller.ts create mode 100644 src/modules/dashboard/dashboard.routes.ts create mode 100644 src/modules/dashboard/dashboard.service.ts diff --git a/src/modules/dashboard/dashboard.controller.ts b/src/modules/dashboard/dashboard.controller.ts new file mode 100644 index 0000000..6d4f2f3 --- /dev/null +++ b/src/modules/dashboard/dashboard.controller.ts @@ -0,0 +1,72 @@ +// Dashboard Controller + +import { Request, Response } from 'express'; +import * as DashboardService from './dashboard.service'; +import { User } from '../../types'; + +function isUser(obj: any): obj is User { + return obj && typeof obj === 'object' && 'role' in obj; +} + +/** + * @swagger + * /dashboard/totals: + * get: + * summary: Get dashboard totals + * tags: + * - Dashboard + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Totals by role + */ +export const getTotals = async (req: Request, res: Response) => { + if (!isUser(req.user)) { + return res.status(401).json({ message: 'Unauthorized' }); + } + const result = await DashboardService.getTotals(req.user); + res.json(result); +}; + +/** + * @swagger + * /dashboard/tree-counts: + * get: + * summary: Get dashboard tree counts + * tags: + * - Dashboard + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Tree counts by role + */ +export const getTreeCounts = async (req: Request, res: Response) => { + if (!isUser(req.user)) { + return res.status(401).json({ message: 'Unauthorized' }); + } + const result = await DashboardService.getTreeCounts(req.user); + res.json(result); +}; + +/** + * @swagger + * /dashboard/scan-stats: + * get: + * summary: Get dashboard scan stats + * tags: + * - Dashboard + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Scan stats by role + */ +export const getScanStats = async (req: Request, res: Response) => { + if (!isUser(req.user)) { + return res.status(401).json({ message: 'Unauthorized' }); + } + const result = await DashboardService.getScanStats(req.user); + res.json(result); +}; diff --git a/src/modules/dashboard/dashboard.routes.ts b/src/modules/dashboard/dashboard.routes.ts new file mode 100644 index 0000000..ed29a5c --- /dev/null +++ b/src/modules/dashboard/dashboard.routes.ts @@ -0,0 +1,13 @@ + +import { Router } from 'express'; +import { getTotals, getTreeCounts, getScanStats } from './dashboard.controller'; +import { authMiddleware } from '../../middleware/auth.middleware'; + + +const router = Router(); + +router.get('/totals', authMiddleware, getTotals); +router.get('/tree-counts', authMiddleware, getTreeCounts); +router.get('/scan-stats', authMiddleware, getScanStats); + +export default router; diff --git a/src/modules/dashboard/dashboard.service.ts b/src/modules/dashboard/dashboard.service.ts new file mode 100644 index 0000000..e0bd4dc --- /dev/null +++ b/src/modules/dashboard/dashboard.service.ts @@ -0,0 +1,111 @@ +// Dashboard Service + +import { User } from '../../types'; +import { prisma } from '../../lib/prisma'; + +export async function getTotals(user: User) { + // Example: Admin sees all, others see limited + let totalUsers = 0; + let totalProjects = 0; + let totalTrees = 0; + let totalPartners = 0; + + if (user.role === 'ADMIN') { + totalUsers = await prisma.user.count(); + totalProjects = await prisma.project.count(); + totalTrees = await prisma.treeScan.count(); + totalPartners = await prisma.partner.count(); + } else { + // For non-admin, return only their projects/trees (customize as needed) + totalUsers = 1; + totalProjects = await prisma.userProject.count({ where: { userId: user.id } }); + totalTrees = await prisma.treeScan.count({ where: { farmerId: user.id } }); + totalPartners = 0; + } + + return { + totalUsers, + totalProjects, + totalTrees, + totalPartners, + role: user.role + }; +} + + +export async function getTreeCounts(user: User) { + // For admin: count trees by species + if (user.role === 'ADMIN') { + const speciesCounts = await prisma.treeType.findMany({ + select: { + name: true, + _count: { + select: { treeScans: true } + } + } + }); + const species = speciesCounts.map(s => ({ name: s.name, count: s._count.treeScans })); + const total = species.reduce((sum, s) => sum + s.count, 0); + return { species, total, role: user.role }; + } else { + // For non-admin, only their trees + const userTrees = await prisma.treeScan.findMany({ + where: { farmerId: user.id }, + select: { species: { select: { name: true } } } + }); + const counts: Record = {}; + userTrees.forEach(t => { + const name = t.species?.name || 'Unknown'; + counts[name] = (counts[name] || 0) + 1; + }); + const species = Object.entries(counts).map(([name, count]) => ({ name, count })); + const total = userTrees.length; + return { species, total, role: user.role }; + } +} + + +export async function getScanStats(user: User) { + // For admin: all scan stats + if (user.role === 'ADMIN') { + const totalScans = await prisma.treeScan.count(); + const today = new Date(); + today.setHours(0, 0, 0, 0); + const scansToday = await prisma.treeScan.count({ + where: { createdAt: { gte: today } } + }); + // Example: status field not in schema, so use isValid/isArchived as proxy + const pending = await prisma.treeScan.count({ where: { isValid: false } }); + const approved = await prisma.treeScan.count({ where: { isValid: true, isArchived: false } }); + const rejected = await prisma.treeScan.count({ where: { isArchived: true } }); + return { + totalScans, + scansToday, + scansByStatus: { + pending, + approved, + rejected + }, + role: user.role + }; + } else { + // For non-admin, only their scans + const totalScans = await prisma.treeScan.count({ where: { farmerId: user.id } }); + const today = new Date(); + today.setHours(0, 0, 0, 0); + const scansToday = await prisma.treeScan.count({ where: { farmerId: user.id, createdAt: { gte: today } } }); + const pending = await prisma.treeScan.count({ where: { farmerId: user.id, isValid: false } }); + const approved = await prisma.treeScan.count({ where: { farmerId: user.id, isValid: true, isArchived: false } }); + const rejected = await prisma.treeScan.count({ where: { farmerId: user.id, isArchived: true } }); + return { + totalScans, + scansToday, + scansByStatus: { + pending, + approved, + rejected + }, + role: user.role + }; + } +} diff --git a/src/routes/index.ts b/src/routes/index.ts index 3ad61b6..570a7c4 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -10,7 +10,9 @@ import { adoptersRouter } from "../modules/adopters"; import { userProjectAssignmentRoutes } from "../modules/user-project-assignment"; import { partnersRoutes } from "../modules/partners"; + import treeScansRoutes from "../modules/tree-scans"; +import dashboardRoutes from "../modules/dashboard/dashboard.routes"; const router = Router(); @@ -27,4 +29,6 @@ router.use("/partners", partnersRoutes); router.use("/tree-scans", treeScansRoutes); +router.use("/dashboard", dashboardRoutes); + export default router; diff --git a/tests/integration/dashboard-widgets.test.ts b/tests/integration/dashboard-widgets.test.ts index 73e2381..2fdd676 100644 --- a/tests/integration/dashboard-widgets.test.ts +++ b/tests/integration/dashboard-widgets.test.ts @@ -1,5 +1,42 @@ -describe.skip("placeholder", () => { - it("to be implemented", () => { - expect(true).toBe(true); + + +import request from 'supertest'; +const API_URL = 'http://localhost:3000'; +const ADMIN_TOKEN = 'dev-admin-token'; + + + +describe('Dashboard Endpoints', () => { + it('GET /dashboard/totals should return totals by role', async () => { + const res = await request(API_URL) + .get('/dashboard/totals') + .set('Authorization', `Bearer ${ADMIN_TOKEN}`); + if (res.statusCode !== 200) { + console.log('Response for /dashboard/totals:', res.statusCode, res.body); + } + expect(res.statusCode).toBe(200); + expect(res.body).toHaveProperty('role'); + }); + + it('GET /dashboard/tree-counts should return tree counts by role', async () => { + const res = await request(API_URL) + .get('/dashboard/tree-counts') + .set('Authorization', `Bearer ${ADMIN_TOKEN}`); + if (res.statusCode !== 200) { + console.log('Response for /dashboard/tree-counts:', res.statusCode, res.body); + } + expect(res.statusCode).toBe(200); + expect(res.body).toHaveProperty('role'); + }); + + it('GET /dashboard/scan-stats should return scan stats by role', async () => { + const res = await request(API_URL) + .get('/dashboard/scan-stats') + .set('Authorization', `Bearer ${ADMIN_TOKEN}`); + if (res.statusCode !== 200) { + console.log('Response for /dashboard/scan-stats:', res.statusCode, res.body); + } + expect(res.statusCode).toBe(200); + expect(res.body).toHaveProperty('role'); }); }); diff --git a/tests/unit/dashboard-widgets.test.ts b/tests/unit/dashboard-widgets.test.ts index 73e2381..14db1c8 100644 --- a/tests/unit/dashboard-widgets.test.ts +++ b/tests/unit/dashboard-widgets.test.ts @@ -1,5 +1,20 @@ -describe.skip("placeholder", () => { - it("to be implemented", () => { - expect(true).toBe(true); +import * as DashboardService from '../../src/modules/dashboard/dashboard.service'; + +describe('Dashboard Service', () => { + const mockUser = { role: 'Admin' } as any; + + it('getTotals returns correct structure', async () => { + const result = await DashboardService.getTotals(mockUser); + expect(result).toHaveProperty('role', 'Admin'); + }); + + it('getTreeCounts returns correct structure', async () => { + const result = await DashboardService.getTreeCounts(mockUser); + expect(result).toHaveProperty('role', 'Admin'); + }); + + it('getScanStats returns correct structure', async () => { + const result = await DashboardService.getScanStats(mockUser); + expect(result).toHaveProperty('role', 'Admin'); }); }); From 3f80dd49eee615a772cc9970e3391c53073de194 Mon Sep 17 00:00:00 2001 From: KVSNIKHIL1998 Date: Sun, 17 May 2026 03:01:38 +1000 Subject: [PATCH 2/2] fix(dashboard): type safety and correct role checks --- src/modules/dashboard/dashboard.service.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/modules/dashboard/dashboard.service.ts b/src/modules/dashboard/dashboard.service.ts index e0bd4dc..dbe9006 100644 --- a/src/modules/dashboard/dashboard.service.ts +++ b/src/modules/dashboard/dashboard.service.ts @@ -1,6 +1,6 @@ // Dashboard Service -import { User } from '../../types'; +import { User, UserRole } from '../../types'; import { prisma } from '../../lib/prisma'; export async function getTotals(user: User) { @@ -10,7 +10,7 @@ export async function getTotals(user: User) { let totalTrees = 0; let totalPartners = 0; - if (user.role === 'ADMIN') { + if (user.role === UserRole.Admin) { totalUsers = await prisma.user.count(); totalProjects = await prisma.project.count(); totalTrees = await prisma.treeScan.count(); @@ -35,7 +35,7 @@ export async function getTotals(user: User) { export async function getTreeCounts(user: User) { // For admin: count trees by species - if (user.role === 'ADMIN') { + if (user.role === UserRole.Admin) { const speciesCounts = await prisma.treeType.findMany({ select: { name: true, @@ -44,8 +44,8 @@ export async function getTreeCounts(user: User) { } } }); - const species = speciesCounts.map(s => ({ name: s.name, count: s._count.treeScans })); - const total = species.reduce((sum, s) => sum + s.count, 0); + const species = speciesCounts.map((s: { name: string; _count: { treeScans: number } }) => ({ name: s.name, count: s._count.treeScans })); + const total = species.reduce((sum: number, s: { name: string; count: number }) => sum + s.count, 0); return { species, total, role: user.role }; } else { // For non-admin, only their trees @@ -54,11 +54,11 @@ export async function getTreeCounts(user: User) { select: { species: { select: { name: true } } } }); const counts: Record = {}; - userTrees.forEach(t => { + userTrees.forEach((t: { species?: { name?: string } }) => { const name = t.species?.name || 'Unknown'; counts[name] = (counts[name] || 0) + 1; }); - const species = Object.entries(counts).map(([name, count]) => ({ name, count })); + const species = Object.entries(counts).map(([name, count]: [string, number]) => ({ name, count })); const total = userTrees.length; return { species, total, role: user.role }; } @@ -67,7 +67,7 @@ export async function getTreeCounts(user: User) { export async function getScanStats(user: User) { // For admin: all scan stats - if (user.role === 'ADMIN') { + if (user.role === UserRole.Admin) { const totalScans = await prisma.treeScan.count(); const today = new Date(); today.setHours(0, 0, 0, 0);