Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions src/modules/dashboard/dashboard.controller.ts
Original file line number Diff line number Diff line change
@@ -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 {

Check failure on line 7 in src/modules/dashboard/dashboard.controller.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type
return obj && typeof obj === 'object' && 'role' in obj;

Check failure on line 8 in src/modules/dashboard/dashboard.controller.ts

View workflow job for this annotation

GitHub Actions / ci

Unsafe return of an `any` typed value
}

/**
* @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);
};
13 changes: 13 additions & 0 deletions src/modules/dashboard/dashboard.routes.ts
Original file line number Diff line number Diff line change
@@ -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);

Check failure on line 9 in src/modules/dashboard/dashboard.routes.ts

View workflow job for this annotation

GitHub Actions / ci

Promise returned in function argument where a void return was expected
router.get('/tree-counts', authMiddleware, getTreeCounts);

Check failure on line 10 in src/modules/dashboard/dashboard.routes.ts

View workflow job for this annotation

GitHub Actions / ci

Promise returned in function argument where a void return was expected
router.get('/scan-stats', authMiddleware, getScanStats);

Check failure on line 11 in src/modules/dashboard/dashboard.routes.ts

View workflow job for this annotation

GitHub Actions / ci

Promise returned in function argument where a void return was expected

export default router;
111 changes: 111 additions & 0 deletions src/modules/dashboard/dashboard.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Dashboard Service

import { User, UserRole } 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 === UserRole.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 === UserRole.Admin) {
const speciesCounts = await prisma.treeType.findMany({
select: {
name: true,
_count: {
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);
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<string, number> = {};
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]: [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) {
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
};
}
}
4 changes: 4 additions & 0 deletions src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -27,4 +29,6 @@ router.use("/partners", partnersRoutes);

router.use("/tree-scans", treeScansRoutes);

router.use("/dashboard", dashboardRoutes);

export default router;
43 changes: 40 additions & 3 deletions tests/integration/dashboard-widgets.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
21 changes: 18 additions & 3 deletions tests/unit/dashboard-widgets.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
Loading