Skip to content

Commit 353cb4e

Browse files
authored
Merge pull request #142 from Zindiks/claude/add-dashboard-analytics-01KKhDzpEF4cVjRvXB5U6kXB
feat: add comprehensive Dashboard & Analytics system
2 parents b0d71f3 + b46dac9 commit 353cb4e

30 files changed

+4154
-0
lines changed

api/src/bootstrap.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ import assigneeRoutes from "./modules/assignees/assignee.route";
2323
import labelRoutes from "./modules/labels/label.route";
2424
import checklistRoutes from "./modules/checklists/checklist.route";
2525
import searchRoutes from "./modules/search/search.route";
26+
import analyticsRoutes from "./modules/analytics/analytics.route";
27+
import timeTrackingRoutes from "./modules/time-tracking/time-tracking.route";
28+
import sprintRoutes from "./modules/sprints/sprint.route";
29+
import reportRoutes from "./modules/reports/report.route";
2630

2731
import { OrganizationSchema } from "./modules/organizations/organization.schema";
2832
import { BoardSchema } from "./modules/boards/board.schema";
@@ -35,6 +39,9 @@ import { AssigneeSchema } from "./modules/assignees/assignee.schema";
3539
import { LabelSchema } from "./modules/labels/label.schema";
3640
import { ChecklistSchema } from "./modules/checklists/checklist.schema";
3741
import { SearchSchema } from "./modules/search/search.schema";
42+
import { AnalyticsSchema } from "./modules/analytics/analytics.schema";
43+
import { TimeTrackingSchemas } from "./modules/time-tracking/time-tracking.schema";
44+
import { SprintSchemas } from "./modules/sprints/sprint.schema";
3845

3946
import { swaggerDocs } from "./swagger";
4047
import { options } from "./configs/config";
@@ -94,6 +101,18 @@ async function addSchemas(server: FastifyInstance) {
94101
for (const schema of Object.values(SearchSchema)) {
95102
server.addSchema(schema);
96103
}
104+
105+
for (const schema of Object.values(AnalyticsSchema)) {
106+
server.addSchema(schema);
107+
}
108+
109+
for (const schema of Object.values(TimeTrackingSchemas)) {
110+
server.addSchema(schema);
111+
}
112+
113+
for (const schema of Object.values(SprintSchemas)) {
114+
server.addSchema(schema);
115+
}
97116
}
98117

99118
async function registerRoutes(server: FastifyInstance) {
@@ -113,6 +132,10 @@ async function registerRoutes(server: FastifyInstance) {
113132
v1.register(labelRoutes, { prefix: "/labels" });
114133
v1.register(checklistRoutes, { prefix: "/checklists" });
115134
v1.register(searchRoutes, { prefix: "/search" });
135+
v1.register(analyticsRoutes, { prefix: "/analytics" });
136+
v1.register(timeTrackingRoutes, { prefix: "/time-tracking" });
137+
v1.register(sprintRoutes, { prefix: "/sprints" });
138+
v1.register(reportRoutes, { prefix: "/reports" });
116139
},
117140
{ prefix: "/v1" },
118141
);
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { Knex } from "knex";
2+
3+
export async function up(knex: Knex): Promise<void> {
4+
return knex.schema.createTable("sprints", function (table) {
5+
table.uuid("id").primary().defaultTo(knex.raw("gen_random_uuid()"));
6+
table
7+
.uuid("board_id")
8+
.notNullable()
9+
.references("id")
10+
.inTable("boards")
11+
.onDelete("CASCADE");
12+
table.string("name").notNullable();
13+
table.text("goal").nullable();
14+
table.timestamp("start_date").notNullable();
15+
table.timestamp("end_date").notNullable();
16+
table
17+
.enum("status", ["planned", "active", "completed", "cancelled"])
18+
.defaultTo("planned");
19+
table.timestamp("created_at").defaultTo(knex.fn.now());
20+
table.timestamp("updated_at").defaultTo(knex.fn.now());
21+
22+
// Indexes for performance
23+
table.index("board_id");
24+
table.index(["board_id", "status"]);
25+
});
26+
}
27+
28+
export async function down(knex: Knex): Promise<void> {
29+
return knex.schema.dropTable("sprints");
30+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { Knex } from "knex";
2+
3+
export async function up(knex: Knex): Promise<void> {
4+
return knex.schema.createTable("time_logs", function (table) {
5+
table.uuid("id").primary().defaultTo(knex.raw("gen_random_uuid()"));
6+
table
7+
.uuid("card_id")
8+
.notNullable()
9+
.references("id")
10+
.inTable("cards")
11+
.onDelete("CASCADE");
12+
table
13+
.uuid("user_id")
14+
.notNullable()
15+
.references("id")
16+
.inTable("users")
17+
.onDelete("CASCADE");
18+
table.integer("duration_minutes").notNullable(); // Duration in minutes
19+
table.text("description").nullable();
20+
table.timestamp("logged_at").defaultTo(knex.fn.now());
21+
table.timestamp("created_at").defaultTo(knex.fn.now());
22+
23+
// Indexes for performance
24+
table.index("card_id");
25+
table.index("user_id");
26+
table.index("logged_at");
27+
table.index(["card_id", "user_id"]);
28+
});
29+
}
30+
31+
export async function down(knex: Knex): Promise<void> {
32+
return knex.schema.dropTable("time_logs");
33+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { Knex } from "knex";
2+
3+
export async function up(knex: Knex): Promise<void> {
4+
return knex.schema.alterTable("cards", function (table) {
5+
table
6+
.uuid("sprint_id")
7+
.nullable()
8+
.references("id")
9+
.inTable("sprints")
10+
.onDelete("SET NULL");
11+
12+
// Index for filtering cards by sprint
13+
table.index("sprint_id");
14+
});
15+
}
16+
17+
export async function down(knex: Knex): Promise<void> {
18+
return knex.schema.alterTable("cards", function (table) {
19+
table.dropColumn("sprint_id");
20+
});
21+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { FastifyReply, FastifyRequest } from "fastify";
2+
import { AnalyticsService } from "./analytics.service";
3+
import {
4+
PersonalDashboardQuery,
5+
BoardAnalyticsParams,
6+
SprintBurndownParams,
7+
} from "./analytics.schema";
8+
9+
export class AnalyticsController {
10+
constructor(private service: AnalyticsService) {}
11+
12+
/**
13+
* GET /api/v1/analytics/dashboard/personal
14+
* Get personal dashboard data
15+
*/
16+
async getPersonalDashboard(
17+
request: FastifyRequest<{
18+
Querystring: PersonalDashboardQuery;
19+
}>,
20+
reply: FastifyReply
21+
) {
22+
try {
23+
const { organizationId } = request.query;
24+
// @ts-ignore - user is added by auth middleware
25+
const userId = request.user?.id;
26+
27+
if (!userId) {
28+
return reply.code(401).send({ error: "Unauthorized" });
29+
}
30+
31+
const dashboard = await this.service.getPersonalDashboard(
32+
userId,
33+
organizationId
34+
);
35+
return reply.code(200).send(dashboard);
36+
} catch (error) {
37+
request.log.error(error);
38+
return reply.code(500).send({ error: "Internal server error" });
39+
}
40+
}
41+
42+
/**
43+
* GET /api/v1/analytics/board/:boardId
44+
* Get board analytics
45+
*/
46+
async getBoardAnalytics(
47+
request: FastifyRequest<{
48+
Params: BoardAnalyticsParams;
49+
}>,
50+
reply: FastifyReply
51+
) {
52+
try {
53+
const { boardId } = request.params;
54+
const analytics = await this.service.getBoardAnalytics(boardId);
55+
return reply.code(200).send(analytics);
56+
} catch (error) {
57+
request.log.error(error);
58+
return reply.code(500).send({ error: "Internal server error" });
59+
}
60+
}
61+
62+
/**
63+
* GET /api/v1/analytics/sprint/:sprintId/burndown
64+
* Get sprint burndown chart data
65+
*/
66+
async getSprintBurndown(
67+
request: FastifyRequest<{
68+
Params: SprintBurndownParams;
69+
}>,
70+
reply: FastifyReply
71+
) {
72+
try {
73+
const { sprintId } = request.params;
74+
const burndown = await this.service.getSprintBurndown(sprintId);
75+
76+
if (!burndown) {
77+
return reply.code(404).send({ error: "Sprint not found" });
78+
}
79+
80+
return reply.code(200).send(burndown);
81+
} catch (error) {
82+
request.log.error(error);
83+
return reply.code(500).send({ error: "Internal server error" });
84+
}
85+
}
86+
87+
/**
88+
* GET /api/v1/analytics/board/:boardId/velocity
89+
* Get board velocity metrics
90+
*/
91+
async getBoardVelocity(
92+
request: FastifyRequest<{
93+
Params: BoardAnalyticsParams;
94+
}>,
95+
reply: FastifyReply
96+
) {
97+
try {
98+
const { boardId } = request.params;
99+
const velocity = await this.service.getBoardVelocity(boardId);
100+
return reply.code(200).send(velocity);
101+
} catch (error) {
102+
request.log.error(error);
103+
return reply.code(500).send({ error: "Internal server error" });
104+
}
105+
}
106+
107+
/**
108+
* GET /api/v1/analytics/tasks/assigned
109+
* Get user's assigned tasks
110+
*/
111+
async getAssignedTasks(
112+
request: FastifyRequest<{
113+
Querystring: PersonalDashboardQuery;
114+
}>,
115+
reply: FastifyReply
116+
) {
117+
try {
118+
const { organizationId } = request.query;
119+
// @ts-ignore - user is added by auth middleware
120+
const userId = request.user?.id;
121+
122+
if (!userId) {
123+
return reply.code(401).send({ error: "Unauthorized" });
124+
}
125+
126+
const tasks = await this.service.getUserAssignedTasks(
127+
userId,
128+
organizationId
129+
);
130+
return reply.code(200).send(tasks);
131+
} catch (error) {
132+
request.log.error(error);
133+
return reply.code(500).send({ error: "Internal server error" });
134+
}
135+
}
136+
}

0 commit comments

Comments
 (0)