Skip to content

Commit e15ef4e

Browse files
committed
refactor dashboard
1 parent 0278979 commit e15ef4e

File tree

8 files changed

+452
-40
lines changed

8 files changed

+452
-40
lines changed

packages/api/src/authz/service.ts

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,15 +111,44 @@ export class AuthorizationService {
111111
/**
112112
* Get project context including organization and visibility
113113
*/
114-
async getProjectContext(projectId: string): Promise<ProjectContext> {
114+
async getProjectContext(
115+
projectId: string,
116+
organizationId?: string
117+
): Promise<ProjectContext> {
115118
try {
119+
console.log(
120+
`[AUTHZ] Getting project context for projectId: ${projectId}, organizationId: ${organizationId}`
121+
);
122+
123+
const whereConditions = organizationId
124+
? and(
125+
eq(project.id, projectId),
126+
eq(project.organizationId, organizationId)
127+
)
128+
: eq(project.id, projectId);
129+
116130
const [projectData] = await db
117131
.select()
118132
.from(project)
119-
.where(eq(project.id, projectId))
133+
.where(whereConditions)
120134
.limit(1);
121135

136+
console.log(
137+
`[AUTHZ] Project query result:`,
138+
projectData
139+
? {
140+
id: projectData.id,
141+
organizationId: projectData.organizationId,
142+
name: projectData.name,
143+
visibility: projectData.visibility,
144+
}
145+
: "null"
146+
);
147+
122148
if (!projectData) {
149+
console.log(
150+
`[AUTHZ] Project not found for projectId: ${projectId}, organizationId: ${organizationId}`
151+
);
123152
throw new ApiError("Project not found", 404);
124153
}
125154

@@ -141,10 +170,14 @@ export class AuthorizationService {
141170
async canAccessProject(
142171
userId: string,
143172
projectId: string,
144-
action: Permission
173+
action: Permission,
174+
organizationId?: string
145175
): Promise<boolean> {
146176
try {
147-
const projectContext = await this.getProjectContext(projectId);
177+
const projectContext = await this.getProjectContext(
178+
projectId,
179+
organizationId
180+
);
148181
const userContext = await this.getUserContext(
149182
userId,
150183
projectContext.organizationId
@@ -201,9 +234,16 @@ export class AuthorizationService {
201234
/**
202235
* Check if user can view project based on visibility
203236
*/
204-
async canViewProject(userId: string, projectId: string): Promise<boolean> {
237+
async canViewProject(
238+
userId: string,
239+
projectId: string,
240+
organizationId?: string
241+
): Promise<boolean> {
205242
try {
206-
const projectContext = await this.getProjectContext(projectId);
243+
const projectContext = await this.getProjectContext(
244+
projectId,
245+
organizationId
246+
);
207247
const userContext = await this.getUserContext(
208248
userId,
209249
projectContext.organizationId

packages/api/src/middleware/authorization.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,17 @@ export function requirePermission(options: AuthorizationOptions) {
3333

3434
if (options.resource === "project") {
3535
if (options.action === "read") {
36-
hasPermission = await authzService.canViewProject(userId, resourceId);
36+
hasPermission = await authzService.canViewProject(
37+
userId,
38+
resourceId,
39+
organizationId
40+
);
3741
} else {
3842
hasPermission = await authzService.canAccessProject(
3943
userId,
4044
resourceId,
41-
options.action
45+
options.action,
46+
organizationId
4247
);
4348
}
4449
} else if (options.resource === "organization" && organizationId) {
@@ -72,6 +77,8 @@ export function requireProjectAccess(action: Permission = "read") {
7277
resource: "project",
7378
action,
7479
getResourceId: (req) => req.params.projectId as string,
80+
getOrganizationId: (req) =>
81+
req.organizationId || (req.params.organizationId as string),
7582
});
7683
}
7784

packages/api/src/routes/projects.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ import {
1919
createProject,
2020
updateProject,
2121
} from "../services/db/projects";
22+
import {
23+
getDashboardStats,
24+
getZones,
25+
getProjectCentroidGeoJSON,
26+
getLocationsForProject,
27+
getProjectTeamMembers,
28+
} from "../services/db/dashboard";
2229
import { ApiError } from "../types";
2330
import { projectInsertSchema } from "@common/db/schema/project";
2431

@@ -144,4 +151,87 @@ router.get(
144151
})
145152
);
146153

154+
// Dashboard routes
155+
// Get dashboard statistics for a project
156+
router.get(
157+
"/:projectId/dashboard/stats",
158+
extractOrganizationContext,
159+
requireProjectView(),
160+
validateParams(getProjectParams),
161+
asyncHandler(async (req, res) => {
162+
const { projectId } = req.params as { projectId: string };
163+
164+
const stats = await getDashboardStats(projectId);
165+
166+
sendSuccess(res, stats, "Dashboard statistics retrieved successfully");
167+
})
168+
);
169+
170+
// Get zones for a project
171+
router.get(
172+
"/:projectId/dashboard/zones",
173+
extractOrganizationContext,
174+
requireProjectView(),
175+
validateParams(getProjectParams),
176+
asyncHandler(async (req, res) => {
177+
const { projectId } = req.params as { projectId: string };
178+
179+
const zones = await getZones(projectId);
180+
181+
sendSuccess(res, zones, "Zones retrieved successfully");
182+
})
183+
);
184+
185+
// Get project centroid for map
186+
router.get(
187+
"/:projectId/dashboard/centroid",
188+
extractOrganizationContext,
189+
requireProjectView(),
190+
validateParams(getProjectParams),
191+
asyncHandler(async (req, res) => {
192+
const { projectId } = req.params as { projectId: string };
193+
194+
const centroid = await getProjectCentroidGeoJSON(projectId);
195+
196+
sendSuccess(res, centroid, "Project centroid retrieved successfully");
197+
})
198+
);
199+
200+
// Get locations for a project
201+
router.get(
202+
"/:projectId/dashboard/locations",
203+
extractOrganizationContext,
204+
requireProjectView(),
205+
validateParams(getProjectParams),
206+
asyncHandler(async (req, res) => {
207+
const { projectId } = req.params as { projectId: string };
208+
209+
const locations = await getLocationsForProject(projectId);
210+
211+
sendSuccess(res, locations, "Locations retrieved successfully");
212+
})
213+
);
214+
215+
// Get project team members
216+
router.get(
217+
"/:projectId/dashboard/team",
218+
extractOrganizationContext,
219+
requireProjectView(),
220+
validateParams(getProjectParams),
221+
asyncHandler(async (req, res) => {
222+
const { projectId } = req.params as { projectId: string };
223+
const page = parseInt(req.query.page as string) || 1;
224+
const pageSize = parseInt(req.query.pageSize as string) || 100;
225+
const search = req.query.search as string;
226+
227+
const team = await getProjectTeamMembers(projectId, {
228+
search,
229+
page,
230+
pageSize,
231+
});
232+
233+
sendSuccess(res, team, "Project team members retrieved successfully");
234+
})
235+
);
236+
147237
export default router;
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { db } from "./database";
2+
import { eq, countDistinct, avg, min, max, sql } from "drizzle-orm";
3+
import { zone } from "@common/db/schema/zone";
4+
import { location_details } from "@common/db/schema/data/location_details";
5+
import { sample_information } from "@common/db/schema/data/sample_information";
6+
import { standard_penetration_test_results } from "@common/db/schema/data/standard_penetration_test_results";
7+
import { userProject, project } from "@common/db/schema/project";
8+
import { user } from "@common/db/schema/user";
9+
10+
// Dashboard statistics
11+
export async function getDashboardStats(projectId: string) {
12+
const [numberOfSamples, avgDepth, numberOfBoreholes, sptStats] =
13+
await Promise.all([
14+
getNumberOfSamples(projectId),
15+
getAvgDepthOfBorehole(projectId),
16+
getNumberOfBoreholes(projectId),
17+
getSptNValueStats(projectId),
18+
]);
19+
20+
return {
21+
numberOfSamples,
22+
avgDepth,
23+
numberOfBoreholes,
24+
sptStats,
25+
};
26+
}
27+
28+
export async function getNumberOfSamples(projectId: string): Promise<number> {
29+
const result = await db
30+
.select({ count: countDistinct(sample_information.id) })
31+
.from(sample_information)
32+
.innerJoin(
33+
location_details,
34+
eq(sample_information.locationDetailsId, location_details.id)
35+
)
36+
.where(eq(location_details.projectId, projectId));
37+
return Number(result[0]?.count) || 0;
38+
}
39+
40+
export async function getAvgDepthOfBorehole(
41+
projectId: string
42+
): Promise<number> {
43+
const result = await db
44+
.select({ avg: avg(location_details.finalDepth) })
45+
.from(location_details)
46+
.where(eq(location_details.projectId, projectId));
47+
return Number(result[0]?.avg) || 0;
48+
}
49+
50+
export async function getNumberOfBoreholes(projectId: string): Promise<number> {
51+
const result = await db
52+
.select({ count: countDistinct(location_details.id) })
53+
.from(location_details)
54+
.where(eq(location_details.projectId, projectId));
55+
return Number(result[0]?.count) || 0;
56+
}
57+
58+
export async function getSptNValueStats(
59+
projectId: string
60+
): Promise<{ min: number; avg: number; max: number }> {
61+
const result = await db
62+
.select({
63+
min: min(standard_penetration_test_results.sptNValue),
64+
avg: avg(standard_penetration_test_results.sptNValue),
65+
max: max(standard_penetration_test_results.sptNValue),
66+
})
67+
.from(standard_penetration_test_results)
68+
.innerJoin(
69+
location_details,
70+
eq(
71+
standard_penetration_test_results.locationDetailsId,
72+
location_details.id
73+
)
74+
)
75+
.where(eq(location_details.projectId, projectId));
76+
77+
return {
78+
min: Number(result[0]?.min) || 0,
79+
avg: Number(result[0]?.avg) || 0,
80+
max: Number(result[0]?.max) || 0,
81+
};
82+
}
83+
84+
// Zones
85+
export async function getZones(projectId: string) {
86+
const zones = await db
87+
.select()
88+
.from(zone)
89+
.where(eq(zone.projectId, projectId));
90+
return zones;
91+
}
92+
93+
// Map data
94+
export async function getProjectCentroidGeoJSON(
95+
projectId: string
96+
): Promise<GeoJSON.Point | null> {
97+
const locationsSubQuery = db
98+
.select({ geometry: location_details.geometry })
99+
.from(location_details)
100+
.where(eq(location_details.projectId, projectId))
101+
.as("project_locations");
102+
103+
const [result] = await db
104+
.select({
105+
geojson: sql<string>`ST_AsGeoJSON(ST_Centroid(ST_Collect(geometry)))`,
106+
})
107+
.from(locationsSubQuery)
108+
.execute();
109+
110+
if (!result?.geojson) {
111+
return null;
112+
}
113+
114+
const geojson = JSON.parse(result.geojson) as GeoJSON.Point;
115+
116+
// Validate that the parsed result is a GeoJSON Point
117+
if (geojson.type !== "Point" || !Array.isArray(geojson.coordinates)) {
118+
return null;
119+
}
120+
121+
return geojson;
122+
}
123+
124+
export async function getLocationsForProject(projectId: string) {
125+
const locations = await db
126+
.select()
127+
.from(location_details)
128+
.where(eq(location_details.projectId, projectId));
129+
return locations;
130+
}
131+
132+
// Project team members
133+
export async function getProjectTeamMembers(
134+
projectId: string,
135+
options: {
136+
search?: string;
137+
page?: number;
138+
pageSize?: number;
139+
} = {}
140+
) {
141+
const { search, page = 1, pageSize = 10 } = options;
142+
const offset = (page - 1) * pageSize;
143+
144+
const teamMembers = await db
145+
.select({
146+
user: user,
147+
role: userProject.role,
148+
total: sql<number>`count(*) over()`,
149+
})
150+
.from(userProject)
151+
.innerJoin(user, eq(user.id, userProject.userId))
152+
.innerJoin(project, eq(project.id, userProject.projectId))
153+
.where(
154+
eq(userProject.projectId, projectId)
155+
// Note: search functionality would need to be added here if needed
156+
)
157+
.limit(pageSize)
158+
.offset(offset)
159+
.orderBy(userProject.role);
160+
161+
if (teamMembers.length === 0) {
162+
return {
163+
users: [],
164+
pagination: {
165+
total: 0,
166+
pageSize,
167+
page,
168+
totalPages: 0,
169+
},
170+
};
171+
}
172+
173+
return {
174+
users: teamMembers.map(({ user, role }) => ({
175+
user,
176+
role,
177+
})),
178+
pagination: {
179+
total: teamMembers[0].total,
180+
pageSize,
181+
page,
182+
totalPages: Math.ceil(teamMembers[0].total / pageSize),
183+
},
184+
};
185+
}

0 commit comments

Comments
 (0)