Skip to content

Commit ccf1af6

Browse files
committed
feat: add direct messaging component with socket integration
- Implemented DirectMessages component for real-time messaging between team members. - Added socket.io integration for sending and receiving messages, marking messages as read, and handling message deletions. - Included UI for displaying message threads and user presence status. feat: create integrations panel for managing webhooks - Developed IntegrationsPanel component to manage outgoing webhook endpoints for workspace events. - Added functionality to add, remove, and toggle the status of webhooks. - Implemented event selection for webhooks with user feedback via toast notifications. feat: introduce sprint board for task management - Created SprintBoard component to manage sprints and associated tasks. - Added functionality to create, delete, and update sprint statuses. - Implemented task assignment to sprints and progress tracking. feat: add chat notification store for managing notifications - Implemented useChatNotifStore for handling chat notifications with Zustand. - Added methods to push, clear, and dismiss notifications. feat: create presence store for managing user online status - Developed usePresenceStore to track user presence status (online/offline). - Added methods for setting individual presence and bulk updating from presence list payloads.
1 parent b5b05ca commit ccf1af6

37 files changed

Lines changed: 4701 additions & 208 deletions

backend/prisma/schema.prisma

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,11 @@ model Task {
136136
dueDate String?
137137
finishDate DateTime?
138138
priority String?
139+
labels Json? // Array of { name: string, color: string }
140+
sprintId String?
141+
dependencies Json? // Array of { taskId: string, type: "blocked_by" | "blocks" }
142+
timeEntries Json? // Array of { memberId, memberName, start, end?, duration? }
143+
recurrence Json? // { pattern: "daily"|"weekly"|"monthly"|"custom", interval?: number, daysOfWeek?: number[], nextDue?: string }
139144
140145
comments Comment[] @relation("TaskComments")
141146
@@ -196,12 +201,61 @@ model GroupChatMessage {
196201
content String @db.Text
197202
attachments Json?
198203
replyTo Json?
204+
isPinned Boolean @default(false)
199205
createdAt DateTime @default(now())
200206
201207
@@index([workspaceId, createdAt])
202208
@@map("group_chat_message")
203209
}
204210

211+
model ActivityLog {
212+
id String @id @default(cuid())
213+
workspaceId String
214+
memberId String?
215+
memberName String?
216+
action String // e.g. "task.created", "task.status_changed", "project.created", "member.added"
217+
entity String? // e.g. "task", "project", "member", "comment"
218+
entityId String?
219+
entityName String? // human-readable, e.g. task title or project name
220+
meta Json? // extra details, e.g. { from: "todo", to: "done" }
221+
createdAt DateTime @default(now())
222+
223+
@@index([workspaceId, createdAt])
224+
@@map("activity_log")
225+
}
226+
227+
model Sprint {
228+
id String @id @default(uuid())
229+
workspaceId String
230+
name String
231+
description String?
232+
status String @default("planning") // planning, active, completed
233+
startDate String?
234+
endDate String?
235+
createdAt DateTime @default(now())
236+
updatedAt DateTime?
237+
238+
@@index([workspaceId])
239+
@@map("sprint")
240+
}
241+
242+
model DirectMessage {
243+
id String @id @default(cuid())
244+
workspaceId String
245+
senderId String
246+
senderName String
247+
senderPhoto String?
248+
receiverId String
249+
content String @db.Text
250+
attachments Json?
251+
isRead Boolean @default(false)
252+
createdAt DateTime @default(now())
253+
254+
@@index([workspaceId, senderId, receiverId])
255+
@@index([workspaceId, receiverId, isRead])
256+
@@map("direct_message")
257+
}
258+
205259
enum Role {
206260
USER
207261
ADMIN
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Controller, Get, Query, UseGuards } from "@nestjs/common";
2+
import { ActivityLogService } from "./activity-log.service";
3+
import { JwtGuard } from "src/common/guards/jwt.guard";
4+
5+
@Controller("api/activity-log")
6+
@UseGuards(JwtGuard)
7+
export class ActivityLogController {
8+
constructor(private readonly svc: ActivityLogService) {}
9+
10+
@Get()
11+
async list(
12+
@Query("workspaceId") workspaceId: string,
13+
@Query("limit") limit?: string,
14+
@Query("cursor") cursor?: string,
15+
) {
16+
if (!workspaceId) return [];
17+
return this.svc.getByWorkspace(
18+
workspaceId,
19+
limit ? parseInt(limit, 10) : 100,
20+
cursor || undefined,
21+
);
22+
}
23+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Module } from "@nestjs/common";
2+
import { ActivityLogController } from "./activity-log.controller";
3+
import { ActivityLogService } from "./activity-log.service";
4+
5+
@Module({
6+
controllers: [ActivityLogController],
7+
providers: [ActivityLogService],
8+
exports: [ActivityLogService],
9+
})
10+
export class ActivityLogModule {}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Injectable } from "@nestjs/common";
2+
import { PrismaClient } from "@prisma/client";
3+
4+
const prisma = new PrismaClient();
5+
6+
export interface LogActivityDto {
7+
workspaceId: string;
8+
memberId?: string;
9+
memberName?: string;
10+
action: string;
11+
entity?: string;
12+
entityId?: string;
13+
entityName?: string;
14+
meta?: Record<string, any>;
15+
}
16+
17+
@Injectable()
18+
export class ActivityLogService {
19+
async log(dto: LogActivityDto) {
20+
return prisma.activityLog.create({
21+
data: {
22+
workspaceId: dto.workspaceId,
23+
memberId: dto.memberId ?? null,
24+
memberName: dto.memberName ?? null,
25+
action: dto.action,
26+
entity: dto.entity ?? null,
27+
entityId: dto.entityId ?? null,
28+
entityName: dto.entityName ?? null,
29+
meta: dto.meta ?? undefined,
30+
},
31+
});
32+
}
33+
34+
async getByWorkspace(workspaceId: string, limit = 100, cursor?: string) {
35+
const where: any = { workspaceId };
36+
37+
const items = await prisma.activityLog.findMany({
38+
where,
39+
orderBy: { createdAt: "desc" },
40+
take: limit,
41+
...(cursor ? { skip: 1, cursor: { id: cursor } } : {}),
42+
});
43+
44+
return items.map((i) => ({
45+
...i,
46+
createdAt: i.createdAt.toISOString(),
47+
}));
48+
}
49+
}

backend/src/app.module.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ import { UploadModule } from "./upload/upload.module";
1717
import { ProjectManagementModule } from "./project-management/project-management.module";
1818
import { EmailModule } from "./email/email.module";
1919
import { GroupChatModule } from "./group-chat/group-chat.module";
20+
import { ActivityLogModule } from "./activity-log/activity-log.module";
21+
import { DashboardModule } from "./dashboard/dashboard.module";
22+
import { SprintModule } from "./sprint/sprint.module";
23+
import { DirectMessageModule } from "./direct-message/direct-message.module";
2024
import { APP_GUARD } from "@nestjs/core";
2125

2226
@Module({
@@ -37,6 +41,10 @@ import { APP_GUARD } from "@nestjs/core";
3741
ProjectManagementModule,
3842
EmailModule,
3943
GroupChatModule,
44+
ActivityLogModule,
45+
DashboardModule,
46+
SprintModule,
47+
DirectMessageModule,
4048
],
4149
controllers: [AppController, AskController],
4250
providers: [
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Controller, Get, Query, UseGuards } from "@nestjs/common";
2+
import { DashboardService } from "./dashboard.service";
3+
import { JwtGuard } from "src/common/guards/jwt.guard";
4+
5+
@Controller("api/dashboard")
6+
@UseGuards(JwtGuard)
7+
export class DashboardController {
8+
constructor(private readonly svc: DashboardService) {}
9+
10+
@Get()
11+
async stats(@Query("workspaceId") workspaceId: string) {
12+
if (!workspaceId) return {};
13+
return this.svc.getStats(workspaceId);
14+
}
15+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Module } from "@nestjs/common";
2+
import { DashboardController } from "./dashboard.controller";
3+
import { DashboardService } from "./dashboard.service";
4+
5+
@Module({
6+
controllers: [DashboardController],
7+
providers: [DashboardService],
8+
})
9+
export class DashboardModule {}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { Injectable } from "@nestjs/common";
2+
import { PrismaClient } from "@prisma/client";
3+
4+
const prisma = new PrismaClient();
5+
6+
@Injectable()
7+
export class DashboardService {
8+
async getStats(workspaceId: string) {
9+
// Get all projects in workspace
10+
const projects = await prisma.project.findMany({
11+
where: { workspaceId, isTrash: false },
12+
select: { id: true, name: true },
13+
});
14+
15+
const projectIds = projects.map((p) => p.id);
16+
17+
// Get all active tasks
18+
const tasks = await prisma.task.findMany({
19+
where: {
20+
isTrash: false,
21+
projectId: { in: projectIds },
22+
},
23+
select: {
24+
id: true,
25+
status: true,
26+
priority: true,
27+
projectId: true,
28+
dueDate: true,
29+
createdAt: true,
30+
finishDate: true,
31+
taskAssignees: {
32+
select: {
33+
memberId: true,
34+
member: { select: { name: true, photo: true } },
35+
},
36+
},
37+
},
38+
});
39+
40+
// Get team members
41+
const team = await prisma.teamMember.findMany({
42+
where: { workspaceId, isTrash: false },
43+
select: { id: true, name: true, photo: true, role: true },
44+
});
45+
46+
// ── Compute stats ──
47+
const statusCounts: Record<string, number> = {};
48+
const priorityCounts: Record<string, number> = {};
49+
const projectTaskCounts: Record<string, number> = {};
50+
const memberTaskCounts: Record<string, number> = {};
51+
let overdue = 0;
52+
let dueToday = 0;
53+
let dueSoon = 0; // within 3 days
54+
55+
const now = new Date();
56+
const todayStr = now.toISOString().slice(0, 10);
57+
const soonDate = new Date();
58+
soonDate.setDate(now.getDate() + 3);
59+
const soonStr = soonDate.toISOString().slice(0, 10);
60+
61+
// Tasks created in last 7 days (for velocity)
62+
const weekAgo = new Date();
63+
weekAgo.setDate(now.getDate() - 7);
64+
let createdThisWeek = 0;
65+
let completedThisWeek = 0;
66+
67+
for (const t of tasks) {
68+
// Status count
69+
statusCounts[t.status] = (statusCounts[t.status] || 0) + 1;
70+
71+
// Priority count
72+
const pri = t.priority ?? "none";
73+
priorityCounts[pri] = (priorityCounts[pri] || 0) + 1;
74+
75+
// Project count
76+
if (t.projectId) {
77+
projectTaskCounts[t.projectId] =
78+
(projectTaskCounts[t.projectId] || 0) + 1;
79+
}
80+
81+
// Member workload
82+
for (const a of t.taskAssignees) {
83+
memberTaskCounts[a.memberId] = (memberTaskCounts[a.memberId] || 0) + 1;
84+
}
85+
86+
// Due dates
87+
if (t.dueDate && t.status !== "done") {
88+
if (t.dueDate < todayStr) overdue++;
89+
else if (t.dueDate === todayStr) dueToday++;
90+
else if (t.dueDate <= soonStr) dueSoon++;
91+
}
92+
93+
// Velocity
94+
if (t.createdAt >= weekAgo) createdThisWeek++;
95+
if (t.status === "done" && t.finishDate && t.finishDate >= weekAgo) {
96+
completedThisWeek++;
97+
}
98+
}
99+
100+
// Build structured response
101+
return {
102+
totalTasks: tasks.length,
103+
totalProjects: projects.length,
104+
totalMembers: team.length,
105+
statusCounts,
106+
priorityCounts,
107+
overdue,
108+
dueToday,
109+
dueSoon,
110+
createdThisWeek,
111+
completedThisWeek,
112+
projectStats: projects.map((p) => ({
113+
id: p.id,
114+
name: p.name,
115+
taskCount: projectTaskCounts[p.id] || 0,
116+
})),
117+
memberWorkload: team.map((m) => ({
118+
id: m.id,
119+
name: m.name,
120+
photo: m.photo,
121+
role: m.role,
122+
taskCount: memberTaskCounts[m.id] || 0,
123+
})),
124+
};
125+
}
126+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {
2+
Controller,
3+
Get,
4+
Query,
5+
DefaultValuePipe,
6+
ParseIntPipe,
7+
Put,
8+
} from "@nestjs/common";
9+
import { DirectMessageService } from "./direct-message.service";
10+
11+
@Controller("api/dm")
12+
export class DirectMessageController {
13+
constructor(private readonly svc: DirectMessageService) {}
14+
15+
@Get("conversation")
16+
getConversation(
17+
@Query("workspaceId") workspaceId: string,
18+
@Query("memberA") memberA: string,
19+
@Query("memberB") memberB: string,
20+
@Query("limit", new DefaultValuePipe(60), ParseIntPipe) limit: number,
21+
) {
22+
return this.svc.getConversation(
23+
workspaceId,
24+
memberA,
25+
memberB,
26+
Math.min(limit, 200),
27+
);
28+
}
29+
30+
@Get("unread")
31+
getUnread(
32+
@Query("workspaceId") workspaceId: string,
33+
@Query("memberId") memberId: string,
34+
) {
35+
return this.svc.getUnreadCounts(workspaceId, memberId);
36+
}
37+
38+
@Put("read")
39+
markRead(
40+
@Query("workspaceId") workspaceId: string,
41+
@Query("receiverId") receiverId: string,
42+
@Query("senderId") senderId: string,
43+
) {
44+
return this.svc.markRead(workspaceId, receiverId, senderId);
45+
}
46+
}

0 commit comments

Comments
 (0)