From 69cc3e75023df2b459ad8c72064b329a41af893a Mon Sep 17 00:00:00 2001 From: KVSNIKHIL1998 Date: Sun, 17 May 2026 02:38:23 +1000 Subject: [PATCH 1/7] 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/7] 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); From c67cb5b8818bea7c2c5296bcfdbc1847c9ae0688 Mon Sep 17 00:00:00 2001 From: KVSNIKHIL1998 Date: Sun, 17 May 2026 12:33:51 +1000 Subject: [PATCH 3/7] Add dashboard API --- src/modules/dashboard/dashboard.controller.ts | 16 ++-- src/modules/dashboard/dashboard.routes.ts | 20 +++-- src/modules/dashboard/dashboard.service.ts | 76 ++++++++++++------- src/routes/index.ts | 1 - tests/integration/dashboard-widgets.test.ts | 50 ++++++------ tests/unit/dashboard-widgets.test.ts | 32 +++++--- 6 files changed, 120 insertions(+), 75 deletions(-) diff --git a/src/modules/dashboard/dashboard.controller.ts b/src/modules/dashboard/dashboard.controller.ts index 6d4f2f3..2cd8411 100644 --- a/src/modules/dashboard/dashboard.controller.ts +++ b/src/modules/dashboard/dashboard.controller.ts @@ -1,11 +1,11 @@ // Dashboard Controller -import { Request, Response } from 'express'; -import * as DashboardService from './dashboard.service'; -import { User } from '../../types'; +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; +function isUser(obj: unknown): obj is User { + return typeof obj === "object" && obj !== null && "role" in obj; } /** @@ -23,7 +23,7 @@ function isUser(obj: any): obj is User { */ export const getTotals = async (req: Request, res: Response) => { if (!isUser(req.user)) { - return res.status(401).json({ message: 'Unauthorized' }); + return res.status(401).json({ message: "Unauthorized" }); } const result = await DashboardService.getTotals(req.user); res.json(result); @@ -44,7 +44,7 @@ export const getTotals = async (req: Request, res: Response) => { */ export const getTreeCounts = async (req: Request, res: Response) => { if (!isUser(req.user)) { - return res.status(401).json({ message: 'Unauthorized' }); + return res.status(401).json({ message: "Unauthorized" }); } const result = await DashboardService.getTreeCounts(req.user); res.json(result); @@ -65,7 +65,7 @@ export const getTreeCounts = async (req: Request, res: Response) => { */ export const getScanStats = async (req: Request, res: Response) => { if (!isUser(req.user)) { - return res.status(401).json({ message: 'Unauthorized' }); + 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 index ed29a5c..32cd93b 100644 --- a/src/modules/dashboard/dashboard.routes.ts +++ b/src/modules/dashboard/dashboard.routes.ts @@ -1,13 +1,17 @@ - -import { Router } from 'express'; -import { getTotals, getTreeCounts, getScanStats } from './dashboard.controller'; -import { authMiddleware } from '../../middleware/auth.middleware'; - +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); +function asyncHandler(fn: (...args: any[]) => Promise) { + return (req: any, res: any, next: any) => { + Promise.resolve(fn(req, res, next)).catch(next); + }; +} + +router.get("/totals", authMiddleware, asyncHandler(getTotals)); +router.get("/tree-counts", authMiddleware, asyncHandler(getTreeCounts)); +router.get("/scan-stats", authMiddleware, asyncHandler(getScanStats)); export default router; diff --git a/src/modules/dashboard/dashboard.service.ts b/src/modules/dashboard/dashboard.service.ts index dbe9006..4e4e60b 100644 --- a/src/modules/dashboard/dashboard.service.ts +++ b/src/modules/dashboard/dashboard.service.ts @@ -1,7 +1,7 @@ // Dashboard Service -import { User, UserRole } from '../../types'; -import { prisma } from '../../lib/prisma'; +import { User, UserRole } from "../../types"; +import { prisma } from "../../lib/prisma"; export async function getTotals(user: User) { // Example: Admin sees all, others see limited @@ -18,7 +18,9 @@ export async function getTotals(user: User) { } else { // For non-admin, return only their projects/trees (customize as needed) totalUsers = 1; - totalProjects = await prisma.userProject.count({ where: { userId: user.id } }); + totalProjects = await prisma.userProject.count({ + where: { userId: user.id }, + }); totalTrees = await prisma.treeScan.count({ where: { farmerId: user.id } }); totalPartners = 0; } @@ -28,11 +30,10 @@ export async function getTotals(user: User) { totalProjects, totalTrees, totalPartners, - role: user.role + role: user.role, }; } - export async function getTreeCounts(user: User) { // For admin: count trees by species if (user.role === UserRole.Admin) { @@ -40,31 +41,40 @@ export async function getTreeCounts(user: User) { select: { name: true, _count: { - select: { treeScans: true } - } - } + select: { treeScans: true }, + }, + }, }); - 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); + 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 const userTrees = await prisma.treeScan.findMany({ where: { farmerId: user.id }, - select: { species: { select: { name: true } } } + select: { species: { select: { name: true } } }, }); const counts: Record = {}; userTrees.forEach((t: { species?: { name?: string } }) => { - const name = t.species?.name || 'Unknown'; + const name = t.species?.name || "Unknown"; counts[name] = (counts[name] || 0) + 1; }); - const species = Object.entries(counts).map(([name, count]: [string, number]) => ({ name, count })); + const species = Object.entries(counts).map( + ([name, count]: [string, number]) => ({ 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 === UserRole.Admin) { @@ -72,40 +82,54 @@ export async function getScanStats(user: User) { const today = new Date(); today.setHours(0, 0, 0, 0); const scansToday = await prisma.treeScan.count({ - where: { createdAt: { gte: today } } + 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 } }); + 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 + rejected, }, - role: user.role + role: user.role, }; } else { // For non-admin, only their scans - const totalScans = await prisma.treeScan.count({ where: { farmerId: user.id } }); + 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 } }); + 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 + rejected, }, - role: user.role + role: user.role, }; } } diff --git a/src/routes/index.ts b/src/routes/index.ts index 570a7c4..cdae387 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -10,7 +10,6 @@ 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"; diff --git a/tests/integration/dashboard-widgets.test.ts b/tests/integration/dashboard-widgets.test.ts index 2fdd676..311f7a2 100644 --- a/tests/integration/dashboard-widgets.test.ts +++ b/tests/integration/dashboard-widgets.test.ts @@ -1,42 +1,46 @@ +import request from "supertest"; +const API_URL = "http://localhost:3000"; +const ADMIN_TOKEN = "dev-admin-token"; - -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 () => { +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}`); + .get("/dashboard/totals") + .set("Authorization", `Bearer ${ADMIN_TOKEN}`); if (res.statusCode !== 200) { - console.log('Response for /dashboard/totals:', res.statusCode, res.body); + console.log("Response for /dashboard/totals:", res.statusCode, res.body); } expect(res.statusCode).toBe(200); - expect(res.body).toHaveProperty('role'); + expect(res.body).toHaveProperty("role"); }); - it('GET /dashboard/tree-counts should return tree counts by role', async () => { + 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}`); + .get("/dashboard/tree-counts") + .set("Authorization", `Bearer ${ADMIN_TOKEN}`); if (res.statusCode !== 200) { - console.log('Response for /dashboard/tree-counts:', res.statusCode, res.body); + console.log( + "Response for /dashboard/tree-counts:", + res.statusCode, + res.body, + ); } expect(res.statusCode).toBe(200); - expect(res.body).toHaveProperty('role'); + expect(res.body).toHaveProperty("role"); }); - it('GET /dashboard/scan-stats should return scan stats by role', async () => { + 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}`); + .get("/dashboard/scan-stats") + .set("Authorization", `Bearer ${ADMIN_TOKEN}`); if (res.statusCode !== 200) { - console.log('Response for /dashboard/scan-stats:', res.statusCode, res.body); + console.log( + "Response for /dashboard/scan-stats:", + res.statusCode, + res.body, + ); } expect(res.statusCode).toBe(200); - expect(res.body).toHaveProperty('role'); + expect(res.body).toHaveProperty("role"); }); }); diff --git a/tests/unit/dashboard-widgets.test.ts b/tests/unit/dashboard-widgets.test.ts index 14db1c8..3383443 100644 --- a/tests/unit/dashboard-widgets.test.ts +++ b/tests/unit/dashboard-widgets.test.ts @@ -1,20 +1,34 @@ -import * as DashboardService from '../../src/modules/dashboard/dashboard.service'; +import * as DashboardService from "../../src/modules/dashboard/dashboard.service"; +import { User, UserRole } from "../../src/types"; -describe('Dashboard Service', () => { - const mockUser = { role: 'Admin' } as any; +describe("Dashboard Service", () => { + const mockUser: User = { + id: 1, + name: "Test Admin", + email: "admin@example.com", + role: UserRole.Admin, + card_id: null, + account_active: true, + can_sign_in: true, + preferred_language: null, + country_id: null, + admin_location_id: null, + created_at: new Date(), + updated_at: new Date(), + }; - it('getTotals returns correct structure', async () => { + it("getTotals returns correct structure", async () => { const result = await DashboardService.getTotals(mockUser); - expect(result).toHaveProperty('role', 'Admin'); + expect(result).toHaveProperty("role", UserRole.Admin); }); - it('getTreeCounts returns correct structure', async () => { + it("getTreeCounts returns correct structure", async () => { const result = await DashboardService.getTreeCounts(mockUser); - expect(result).toHaveProperty('role', 'Admin'); + expect(result).toHaveProperty("role", UserRole.Admin); }); - it('getScanStats returns correct structure', async () => { + it("getScanStats returns correct structure", async () => { const result = await DashboardService.getScanStats(mockUser); - expect(result).toHaveProperty('role', 'Admin'); + expect(result).toHaveProperty("role", UserRole.Admin); }); }); From f4469220226a7957db9f8f08d858d0e90e06ca6b Mon Sep 17 00:00:00 2001 From: KVSNIKHIL1998 Date: Sun, 17 May 2026 12:45:10 +1000 Subject: [PATCH 4/7] Add dashboard API and widgets tests --- src/modules/dashboard/dashboard.controller.ts | 2 +- src/modules/dashboard/dashboard.routes.ts | 2 +- src/modules/dashboard/dashboard.service.ts | 2 +- src/routes/index.ts | 2 +- tests/integration/dashboard-widgets.test.ts | 1 + tests/unit/dashboard-widgets.test.ts | 1 + 6 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/modules/dashboard/dashboard.controller.ts b/src/modules/dashboard/dashboard.controller.ts index 2cd8411..662ebd0 100644 --- a/src/modules/dashboard/dashboard.controller.ts +++ b/src/modules/dashboard/dashboard.controller.ts @@ -10,7 +10,7 @@ function isUser(obj: unknown): obj is User { /** * @swagger - * /dashboard/totals: + * /dashboard/totals: * get: * summary: Get dashboard totals * tags: diff --git a/src/modules/dashboard/dashboard.routes.ts b/src/modules/dashboard/dashboard.routes.ts index 32cd93b..a41e53b 100644 --- a/src/modules/dashboard/dashboard.routes.ts +++ b/src/modules/dashboard/dashboard.routes.ts @@ -14,4 +14,4 @@ router.get("/totals", authMiddleware, asyncHandler(getTotals)); router.get("/tree-counts", authMiddleware, asyncHandler(getTreeCounts)); router.get("/scan-stats", authMiddleware, asyncHandler(getScanStats)); -export default router; +export default router; \ No newline at end of file diff --git a/src/modules/dashboard/dashboard.service.ts b/src/modules/dashboard/dashboard.service.ts index 4e4e60b..ba74a64 100644 --- a/src/modules/dashboard/dashboard.service.ts +++ b/src/modules/dashboard/dashboard.service.ts @@ -1,4 +1,4 @@ -// Dashboard Service +// Dashboard Service import { User, UserRole } from "../../types"; import { prisma } from "../../lib/prisma"; diff --git a/src/routes/index.ts b/src/routes/index.ts index cdae387..f0cd205 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -11,7 +11,7 @@ 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"; +import dashboardRoutes from "../modules/dashboard/dashboard.routes"; //dashboard const router = Router(); diff --git a/tests/integration/dashboard-widgets.test.ts b/tests/integration/dashboard-widgets.test.ts index 311f7a2..9370275 100644 --- a/tests/integration/dashboard-widgets.test.ts +++ b/tests/integration/dashboard-widgets.test.ts @@ -44,3 +44,4 @@ describe("Dashboard Endpoints", () => { expect(res.body).toHaveProperty("role"); }); }); + diff --git a/tests/unit/dashboard-widgets.test.ts b/tests/unit/dashboard-widgets.test.ts index 3383443..bb63712 100644 --- a/tests/unit/dashboard-widgets.test.ts +++ b/tests/unit/dashboard-widgets.test.ts @@ -32,3 +32,4 @@ describe("Dashboard Service", () => { expect(result).toHaveProperty("role", UserRole.Admin); }); }); + From 35c8b20e6fe1a19fb81cc5b647b0dce8c4f349a3 Mon Sep 17 00:00:00 2001 From: KVSNIKHIL1998 Date: Sun, 17 May 2026 12:54:46 +1000 Subject: [PATCH 5/7] fix: dashboard API --- src/modules/dashboard/dashboard.controller.ts | 15 +++++++++------ src/modules/dashboard/dashboard.routes.ts | 10 ++++++---- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/modules/dashboard/dashboard.controller.ts b/src/modules/dashboard/dashboard.controller.ts index 662ebd0..abedc1a 100644 --- a/src/modules/dashboard/dashboard.controller.ts +++ b/src/modules/dashboard/dashboard.controller.ts @@ -21,9 +21,10 @@ function isUser(obj: unknown): obj is User { * 200: * description: Totals by role */ -export const getTotals = async (req: Request, res: Response) => { +export const getTotals = async (req: Request, res: Response): Promise => { if (!isUser(req.user)) { - return res.status(401).json({ message: "Unauthorized" }); + res.status(401).json({ message: "Unauthorized" }); + return; } const result = await DashboardService.getTotals(req.user); res.json(result); @@ -42,9 +43,10 @@ export const getTotals = async (req: Request, res: Response) => { * 200: * description: Tree counts by role */ -export const getTreeCounts = async (req: Request, res: Response) => { +export const getTreeCounts = async (req: Request, res: Response): Promise => { if (!isUser(req.user)) { - return res.status(401).json({ message: "Unauthorized" }); + res.status(401).json({ message: "Unauthorized" }); + return; } const result = await DashboardService.getTreeCounts(req.user); res.json(result); @@ -63,9 +65,10 @@ export const getTreeCounts = async (req: Request, res: Response) => { * 200: * description: Scan stats by role */ -export const getScanStats = async (req: Request, res: Response) => { +export const getScanStats = async (req: Request, res: Response): Promise => { if (!isUser(req.user)) { - return res.status(401).json({ message: "Unauthorized" }); + res.status(401).json({ message: "Unauthorized" }); + return; } 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 index a41e53b..d99c17e 100644 --- a/src/modules/dashboard/dashboard.routes.ts +++ b/src/modules/dashboard/dashboard.routes.ts @@ -1,12 +1,14 @@ -import { Router } from "express"; +import { Router, Request, Response, NextFunction } from "express"; import { getTotals, getTreeCounts, getScanStats } from "./dashboard.controller"; import { authMiddleware } from "../../middleware/auth.middleware"; const router = Router(); -function asyncHandler(fn: (...args: any[]) => Promise) { - return (req: any, res: any, next: any) => { - Promise.resolve(fn(req, res, next)).catch(next); +type AsyncRouteHandler = (req: Request, res: Response, next: NextFunction) => Promise; + +function asyncHandler(fn: AsyncRouteHandler): (req: Request, res: Response, next: NextFunction) => void { + return (req, res, next) => { + void fn(req, res, next).catch(next); }; } From ed8f0f9427c7b954e591ab9c8cb7e1478193a96a Mon Sep 17 00:00:00 2001 From: KVSNIKHIL1998 Date: Sun, 17 May 2026 13:07:37 +1000 Subject: [PATCH 6/7] chore: format dashboard files with Prettier --- src/modules/dashboard/dashboard.controller.ts | 12 +++++++++--- src/modules/dashboard/dashboard.routes.ts | 12 +++++++++--- src/modules/dashboard/dashboard.service.ts | 2 +- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/modules/dashboard/dashboard.controller.ts b/src/modules/dashboard/dashboard.controller.ts index abedc1a..4d8d663 100644 --- a/src/modules/dashboard/dashboard.controller.ts +++ b/src/modules/dashboard/dashboard.controller.ts @@ -10,7 +10,7 @@ function isUser(obj: unknown): obj is User { /** * @swagger - * /dashboard/totals: + * /dashboard/totals: * get: * summary: Get dashboard totals * tags: @@ -43,7 +43,10 @@ export const getTotals = async (req: Request, res: Response): Promise => { * 200: * description: Tree counts by role */ -export const getTreeCounts = async (req: Request, res: Response): Promise => { +export const getTreeCounts = async ( + req: Request, + res: Response, +): Promise => { if (!isUser(req.user)) { res.status(401).json({ message: "Unauthorized" }); return; @@ -65,7 +68,10 @@ export const getTreeCounts = async (req: Request, res: Response): Promise * 200: * description: Scan stats by role */ -export const getScanStats = async (req: Request, res: Response): Promise => { +export const getScanStats = async ( + req: Request, + res: Response, +): Promise => { if (!isUser(req.user)) { res.status(401).json({ message: "Unauthorized" }); return; diff --git a/src/modules/dashboard/dashboard.routes.ts b/src/modules/dashboard/dashboard.routes.ts index d99c17e..5aff2a5 100644 --- a/src/modules/dashboard/dashboard.routes.ts +++ b/src/modules/dashboard/dashboard.routes.ts @@ -4,9 +4,15 @@ import { authMiddleware } from "../../middleware/auth.middleware"; const router = Router(); -type AsyncRouteHandler = (req: Request, res: Response, next: NextFunction) => Promise; +type AsyncRouteHandler = ( + req: Request, + res: Response, + next: NextFunction, +) => Promise; -function asyncHandler(fn: AsyncRouteHandler): (req: Request, res: Response, next: NextFunction) => void { +function asyncHandler( + fn: AsyncRouteHandler, +): (req: Request, res: Response, next: NextFunction) => void { return (req, res, next) => { void fn(req, res, next).catch(next); }; @@ -16,4 +22,4 @@ router.get("/totals", authMiddleware, asyncHandler(getTotals)); router.get("/tree-counts", authMiddleware, asyncHandler(getTreeCounts)); router.get("/scan-stats", authMiddleware, asyncHandler(getScanStats)); -export default router; \ No newline at end of file +export default router; diff --git a/src/modules/dashboard/dashboard.service.ts b/src/modules/dashboard/dashboard.service.ts index ba74a64..4e4e60b 100644 --- a/src/modules/dashboard/dashboard.service.ts +++ b/src/modules/dashboard/dashboard.service.ts @@ -1,4 +1,4 @@ -// Dashboard Service +// Dashboard Service import { User, UserRole } from "../../types"; import { prisma } from "../../lib/prisma"; From 792742428be9916cb7bb6e1e6aa8e4fd57066dc3 Mon Sep 17 00:00:00 2001 From: Kotapati Venkat Sai Nikhil Date: Sun, 17 May 2026 13:24:43 +1000 Subject: [PATCH 7/7] Update pull-request.yaml --- .github/workflows/pull-request.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index 4bb4ab0..56a4a7d 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -61,6 +61,9 @@ jobs: - name: Setup test database run: npx prisma db push + - name: Seed database + run: npx ts-node prisma/seed.ts + - name: Test run: npm run test:coverage -- --runInBand env: @@ -73,4 +76,4 @@ jobs: AUTH_DEV_DEVELOPER_TOKEN: dev-developer-token - name: Build - run: npm run build \ No newline at end of file + run: npm run build