Skip to content

Commit 452135d

Browse files
authored
Feat/implement milestones (#65)
* feat(db): create milestone entity and its relations * feat: implements get milestones route * feat: implements create milestone module * feat: implements update milestone modules * feat: implements get unique milestone modules * feat: implements delete milestone modules * refactor: fix typo in variable name * feat: implements module to return all tasks in a milestone * feat: implements assign tasks to milestone module * feat: implements progress on milestone modules * fix(milestones): ensure task association consistency and cleanup on deletion * feat(tasks): implement two-way consistency for labels and milestones * feat(milestones): implements validation in end date to be equal or after start date * fix(provider): adjust to make dark mode as default theme * fix: address feedback for milestone and task management
1 parent e9ed0fb commit 452135d

34 files changed

Lines changed: 879 additions & 59 deletions

File tree

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,31 @@
11
import { prisma } from "@blaboard/db";
22

33
export async function deleteLabelUseCase(id: string, organizationId: string) {
4-
return await prisma.label.delete({
4+
const label = await prisma.label.delete({
55
where: { id, organizationId },
66
});
7+
8+
if (label.taskIds && label.taskIds.length > 0) {
9+
for (const taskId of label.taskIds) {
10+
const task = await prisma.task.findUnique({
11+
where: { id: taskId, organizationId },
12+
select: {
13+
labelIds: true,
14+
},
15+
});
16+
17+
if (task) {
18+
await prisma.task.update({
19+
where: { id: taskId },
20+
data: {
21+
labelIds: {
22+
set: task.labelIds.filter((lid) => lid !== id),
23+
},
24+
},
25+
});
26+
}
27+
}
28+
}
29+
30+
return label;
731
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Elysia } from "elysia";
2+
import { authMiddleware } from "@/shared/http/middleware/auth.middleware";
3+
import { authMiddlewareErrorSchemas } from "@/shared/schemas/auth-middleware-errors";
4+
import {
5+
assignTasksMilestoneBodySchema,
6+
assignTasksMilestoneParamsSchema,
7+
assignTasksMilestoneResponseSchema,
8+
} from "./schemas";
9+
import { assignTasksMilestone } from "./use-case";
10+
11+
export const assignTasksMilestoneRouter = new Elysia()
12+
.use(authMiddleware)
13+
.patch(
14+
":id/tasks",
15+
async ({ status, params, session, body }) => {
16+
const result = await assignTasksMilestone(
17+
params.id,
18+
session.activeOrganizationId,
19+
body,
20+
);
21+
return status(200, result);
22+
},
23+
{
24+
requireOrganization: true,
25+
body: assignTasksMilestoneBodySchema,
26+
params: assignTasksMilestoneParamsSchema,
27+
response: {
28+
200: assignTasksMilestoneResponseSchema,
29+
...authMiddlewareErrorSchemas,
30+
},
31+
},
32+
);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import z from "zod";
2+
import { milestonesStatusSchema } from "@/shared/schemas/milestone-status";
3+
import { zDate } from "@/shared/schemas/zod-date";
4+
5+
export const assignTasksMilestoneParamsSchema = z.object({
6+
id: z.string().min(1),
7+
});
8+
9+
export const assignTasksMilestoneBodySchema = z.object({
10+
taskIds: z.array(z.string()).min(1),
11+
});
12+
13+
export type AssignTasksMilestoneBody = z.infer<
14+
typeof assignTasksMilestoneBodySchema
15+
>;
16+
17+
export const assignTasksMilestoneResponseSchema = z.object({
18+
id: z.string(),
19+
description: z.string().nullable(),
20+
organizationId: z.string(),
21+
createdAt: zDate,
22+
updatedAt: zDate,
23+
taskIds: z.array(z.string()),
24+
name: z.string(),
25+
status: milestonesStatusSchema,
26+
startDate: zDate.nullable(),
27+
endDate: zDate.nullable(),
28+
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { prisma } from "@blaboard/db";
2+
import type { AssignTasksMilestoneBody } from "./schemas";
3+
4+
export async function assignTasksMilestone(
5+
milestoneId: string,
6+
organizationId: string,
7+
input: AssignTasksMilestoneBody,
8+
) {
9+
return await prisma.$transaction(async (tx) => {
10+
const taskWithMilestone = await tx.task.findMany({
11+
where: {
12+
id: { in: input.taskIds },
13+
organizationId,
14+
milestoneId: { not: null, notIn: [milestoneId] },
15+
},
16+
select: {
17+
id: true,
18+
milestoneId: true,
19+
},
20+
});
21+
22+
for (const task of taskWithMilestone) {
23+
if (task.milestoneId) {
24+
const oldMilestone = await tx.milestone.findUnique({
25+
where: { id: task.milestoneId },
26+
});
27+
28+
if (oldMilestone) {
29+
await tx.milestone.update({
30+
where: { id: task.milestoneId },
31+
data: {
32+
taskIds: {
33+
set: oldMilestone.taskIds.filter((tid) => tid !== task.id),
34+
},
35+
},
36+
});
37+
}
38+
}
39+
}
40+
41+
await tx.task.updateMany({
42+
where: {
43+
id: { in: input.taskIds },
44+
organizationId,
45+
},
46+
data: {
47+
milestoneId: milestoneId,
48+
},
49+
});
50+
51+
const currentMilestone = await tx.milestone.findUnique({
52+
where: { id: milestoneId },
53+
});
54+
55+
const uniqueTaskIds = Array.from(
56+
new Set([...(currentMilestone?.taskIds || []), ...input.taskIds]),
57+
);
58+
59+
return await tx.milestone.update({
60+
where: {
61+
id: milestoneId,
62+
organizationId,
63+
},
64+
data: {
65+
taskIds: { set: uniqueTaskIds },
66+
},
67+
});
68+
});
69+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Elysia } from "elysia";
2+
import { authMiddleware } from "@/shared/http/middleware/auth.middleware";
3+
import { authMiddlewareErrorSchemas } from "@/shared/schemas/auth-middleware-errors";
4+
import {
5+
createMilestonesBodySchema,
6+
createMilestonesResponseSchema,
7+
} from "./schemas";
8+
import { createMilestone } from "./use-case";
9+
10+
export const createMilestoneRouter = new Elysia().use(authMiddleware).post(
11+
"/",
12+
async ({ status, session, body }) => {
13+
const result = await createMilestone(session.activeOrganizationId, body);
14+
return status(201, result);
15+
},
16+
{
17+
requireOrganization: true,
18+
body: createMilestonesBodySchema,
19+
response: {
20+
201: createMilestonesResponseSchema,
21+
...authMiddlewareErrorSchemas,
22+
},
23+
},
24+
);
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import z from "zod";
2+
import { milestonesStatusSchema } from "@/shared/schemas/milestone-status";
3+
import { zDate } from "@/shared/schemas/zod-date";
4+
5+
export const createMilestonesBodySchema = z
6+
.object({
7+
name: z.string().min(2).max(100),
8+
description: z.string().min(2).max(200).nullable(),
9+
status: milestonesStatusSchema,
10+
startDate: zDate.nullable(),
11+
endDate: zDate.nullable(),
12+
})
13+
.refine(
14+
(data) => {
15+
if (data.startDate && data.endDate) {
16+
const start = new Date(data.startDate);
17+
const end = new Date(data.endDate);
18+
return end >= start;
19+
}
20+
return true;
21+
},
22+
{
23+
message: "End date must be after or equal to start date",
24+
path: ["endDate"],
25+
},
26+
);
27+
28+
export type CreateMilestonesInput = z.infer<typeof createMilestonesBodySchema>;
29+
30+
export const createMilestonesResponseSchema = z.object({
31+
id: z.string(),
32+
name: z.string(),
33+
description: z.string().nullable(),
34+
status: milestonesStatusSchema,
35+
startDate: zDate.nullable(),
36+
endDate: zDate.nullable(),
37+
organizationId: z.string(),
38+
taskIds: z.array(z.string()),
39+
createdAt: zDate,
40+
updatedAt: zDate,
41+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { prisma } from "@blaboard/db";
2+
import type { CreateMilestonesInput } from "./schemas";
3+
4+
export async function createMilestone(
5+
organizationId: string,
6+
input: CreateMilestonesInput,
7+
) {
8+
return prisma.milestone.create({
9+
data: {
10+
name: input.name,
11+
description: input.description,
12+
status: input.status,
13+
startDate: input.startDate,
14+
endDate: input.endDate,
15+
organizationId,
16+
},
17+
});
18+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Elysia } from "elysia";
2+
import { authMiddleware } from "@/shared/http/middleware/auth.middleware";
3+
import { authMiddlewareErrorSchemas } from "@/shared/schemas/auth-middleware-errors";
4+
import {
5+
deleteMilestoneParamsSchema,
6+
deleteMilestoneResponseSchema,
7+
} from "./schemas";
8+
import { deleteMilestone } from "./use-case";
9+
10+
export const deleteMilestonesRouter = new Elysia().use(authMiddleware).delete(
11+
"/:id",
12+
async ({ session, params, status }) => {
13+
const result = await deleteMilestone(
14+
params.id,
15+
session.activeOrganizationId,
16+
);
17+
18+
return status(200, result);
19+
},
20+
{
21+
requireOrganization: true,
22+
params: deleteMilestoneParamsSchema,
23+
response: {
24+
200: deleteMilestoneResponseSchema,
25+
...authMiddlewareErrorSchemas,
26+
},
27+
},
28+
);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import z from "zod";
2+
import { milestonesStatusSchema } from "@/shared/schemas/milestone-status";
3+
import { zDate } from "@/shared/schemas/zod-date";
4+
5+
export const deleteMilestoneParamsSchema = z.object({
6+
id: z.string().min(2),
7+
});
8+
9+
export const deleteMilestoneResponseSchema = z.object({
10+
id: z.string(),
11+
name: z.string(),
12+
description: z.string().nullable(),
13+
status: milestonesStatusSchema,
14+
startDate: zDate.nullable(),
15+
endDate: zDate.nullable(),
16+
taskIds: z.array(z.string()),
17+
organizationId: z.string(),
18+
createdAt: zDate,
19+
updatedAt: zDate,
20+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { prisma } from "@blaboard/db";
2+
3+
export async function deleteMilestone(id: string, organizationId: string) {
4+
return await prisma.$transaction(async (tx) => {
5+
await tx.task.updateMany({
6+
where: { milestoneId: id, organizationId },
7+
data: { milestoneId: null },
8+
});
9+
return await tx.milestone.delete({ where: { id, organizationId } });
10+
});
11+
}

0 commit comments

Comments
 (0)