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
5 changes: 4 additions & 1 deletion .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -73,4 +76,4 @@ jobs:
AUTH_DEV_DEVELOPER_TOKEN: dev-developer-token

- name: Build
run: npm run build
run: npm run build
81 changes: 81 additions & 0 deletions src/modules/dashboard/dashboard.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Dashboard Controller

import { Request, Response } from "express";
import * as DashboardService from "./dashboard.service";
import { User } from "../../types";

function isUser(obj: unknown): obj is User {
return typeof obj === "object" && obj !== null && "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): Promise<void> => {
if (!isUser(req.user)) {
res.status(401).json({ message: "Unauthorized" });
return;
}
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,
): Promise<void> => {
if (!isUser(req.user)) {
res.status(401).json({ message: "Unauthorized" });
return;
}
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,
): Promise<void> => {
if (!isUser(req.user)) {
res.status(401).json({ message: "Unauthorized" });
return;
}
const result = await DashboardService.getScanStats(req.user);
res.json(result);
};
25 changes: 25 additions & 0 deletions src/modules/dashboard/dashboard.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Router, Request, Response, NextFunction } from "express";
import { getTotals, getTreeCounts, getScanStats } from "./dashboard.controller";
import { authMiddleware } from "../../middleware/auth.middleware";

const router = Router();

type AsyncRouteHandler = (
req: Request,
res: Response,
next: NextFunction,
) => Promise<void>;

function asyncHandler(
fn: AsyncRouteHandler,
): (req: Request, res: Response, next: NextFunction) => void {
return (req, res, next) => {
void 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;
135 changes: 135 additions & 0 deletions src/modules/dashboard/dashboard.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// 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,
};
}
}
3 changes: 3 additions & 0 deletions src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +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"; //dashboard

const router = Router();

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

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

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

export default router;
48 changes: 45 additions & 3 deletions tests/integration/dashboard-widgets.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,47 @@
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");
});
});

36 changes: 33 additions & 3 deletions tests/unit/dashboard-widgets.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
describe.skip("placeholder", () => {
it("to be implemented", () => {
expect(true).toBe(true);
import * as DashboardService from "../../src/modules/dashboard/dashboard.service";
import { User, UserRole } from "../../src/types";

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 () => {
const result = await DashboardService.getTotals(mockUser);
expect(result).toHaveProperty("role", UserRole.Admin);
});

it("getTreeCounts returns correct structure", async () => {
const result = await DashboardService.getTreeCounts(mockUser);
expect(result).toHaveProperty("role", UserRole.Admin);
});

it("getScanStats returns correct structure", async () => {
const result = await DashboardService.getScanStats(mockUser);
expect(result).toHaveProperty("role", UserRole.Admin);
});
});

Loading