Skip to content

Commit 33238d2

Browse files
committed
feat: my tasks & report
1 parent a87f085 commit 33238d2

12 files changed

Lines changed: 2289 additions & 430 deletions

File tree

backend/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ model Task {
121121
assigneeId String?
122122
startDate String?
123123
dueDate String?
124+
finishDate DateTime?
124125
priority String?
125126
assignee TeamMember? @relation("TaskMembers", fields: [assigneeId], references: [id])
126127
isTrash Boolean @default(false)

backend/src/helpers/safeDate.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export function safeDate(v?: string) {
2+
if (!v) return undefined
3+
const d = new Date(v)
4+
return isNaN(d.getTime()) ? undefined : d
5+
}
6+
7+
export function startOfDay(d: Date) {
8+
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
9+
}
10+
11+
export function endOfDay(d: Date) {
12+
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999);
13+
}
14+
15+
export function parseDateSafe(d?: string): Date | undefined {
16+
if (!d) return undefined;
17+
const dt = new Date(d);
18+
if (Number.isNaN(dt.getTime())) return undefined;
19+
return dt;
20+
}

backend/src/project-management/project-management.controller.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { CreateWorkspaceDto } from "./dto/workspace.dto";
3737
@Controller("api")
3838
@UseGuards(JwtGuard)
3939
export class ProjectManagementController {
40-
constructor(private svc: ProjectManagementService) {}
40+
constructor(private svc: ProjectManagementService) { }
4141

4242
@Get("project-management/workspaces")
4343
getWorkspaces(@Req() req: any) {
@@ -109,8 +109,20 @@ export class ProjectManagementController {
109109

110110
// Tasks
111111
@Get("tasks")
112-
getTasks(@Query("projectId") projectId?: string) {
113-
return this.svc.getTasks(projectId);
112+
getTasks(@Query("projectId") projectId?: string, @Query("startDate") startDate?: string, @Query("endDate") endDate?: string) {
113+
return this.svc.getTasks(projectId, startDate, endDate);
114+
}
115+
116+
// getMyTasks
117+
@Get("tasks/me/:memberId")
118+
getMyTasks(@Param("memberId") memberId: string, @Query("workspaceId") workspaceId?: string, @Query("startDate") startDate?: string, @Query("endDate") endDate?: string) {
119+
return this.svc.getMyTasks(memberId, workspaceId, startDate, endDate);
120+
}
121+
122+
// getMyTasks
123+
@Get("tasks/workspace/:workspaceId")
124+
getTasksWorkspace(@Param("workspaceId") workspaceId: string, @Query("memberId") memberId?: string, @Query("startDate") startDate?: string, @Query("endDate") endDate?: string) {
125+
return this.svc.getTasksWorkspace(workspaceId, memberId, startDate, endDate);
114126
}
115127

116128
@Post("tasks")

backend/src/project-management/project-management.service.ts

Lines changed: 252 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { Prisma } from "@prisma/client";
1313
import { hashPassword } from "src/auth/utils";
1414
import { EmailService } from "src/email/email.service";
1515
import logger from "vico-logger";
16+
import { endOfDay, parseDateSafe, safeDate, startOfDay } from "src/helpers/safeDate";
1617

1718
const prisma = new PrismaClient();
1819

@@ -312,13 +313,37 @@ export class ProjectManagementService {
312313
}
313314

314315
// Tasks
315-
async getTasks(projectId?: string) {
316+
async getTasks(
317+
projectId?: string,
318+
startDate?: string,
319+
endDate?: string
320+
) {
321+
await this.migrateSingleAssigneeToMulti();
322+
323+
const now = new Date();
324+
325+
// 🔥 DEFAULT RANGE: last 60 days
326+
const defaultStart = new Date();
327+
defaultStart.setDate(now.getDate() - 60);
328+
329+
const start =
330+
startDate ? startOfDay(new Date(startDate)) : startOfDay(defaultStart);
331+
332+
const end =
333+
endDate ? endOfDay(new Date(endDate)) : endOfDay(now);
334+
335+
const where: any = {
336+
isTrash: false,
337+
createdAt: {
338+
gte: start,
339+
lte: end,
340+
},
341+
};
316342

317-
await this.migrateSingleAssigneeToMulti()
343+
if (projectId) {
344+
where.projectId = projectId;
345+
}
318346

319-
const where = projectId
320-
? { projectId, isTrash: false }
321-
: { isTrash: false };
322347
const tasks = await prisma.task.findMany({
323348
where,
324349
orderBy: { createdAt: "desc" },
@@ -344,13 +369,204 @@ export class ProjectManagementService {
344369
createdAt: c.createdAt?.toISOString(),
345370
updatedAt: c.updatedAt?.toISOString(),
346371
})),
347-
taskAssignees: t.taskAssignees.map(a => ({
372+
taskAssignees: t.taskAssignees.map((a) => ({
373+
id: a.member.id,
374+
name: a.member.name,
375+
photo: a.member.photo,
376+
phone: a.member.phone,
377+
role: a.member.role,
378+
})),
379+
}));
380+
}
381+
382+
383+
// Tasks
384+
async getMyTasks(
385+
memberId?: string,
386+
workspaceId?: string,
387+
startDate?: string,
388+
endDate?: string
389+
) {
390+
const now = new Date()
391+
392+
// default: 60 hari terakhir
393+
const defaultStart = new Date()
394+
defaultStart.setDate(now.getDate() - 60)
395+
396+
const start = safeDate(startDate)
397+
const end = safeDate(endDate)
398+
399+
const createdAtFilter =
400+
start || end
401+
? {
402+
...(start && { gte: start }),
403+
...(end && { lte: end }),
404+
}
405+
: {
406+
gte: defaultStart,
407+
}
408+
409+
const where = {
410+
isTrash: false,
411+
createdAt: createdAtFilter,
412+
413+
...(workspaceId && {
414+
project: {
415+
workspaceId,
416+
},
417+
}),
418+
419+
...(memberId && {
420+
taskAssignees: {
421+
some: { memberId },
422+
},
423+
}),
424+
}
425+
426+
const tasks = await prisma.task.findMany({
427+
where,
428+
orderBy: [
429+
{
430+
project: {
431+
name: "asc", // urutkan berdasarkan nama project
432+
},
433+
},
434+
{
435+
createdAt: "desc", // lalu task terbaru
436+
},
437+
],
438+
include: {
439+
comments: {
440+
where: { isTrash: false },
441+
orderBy: { createdAt: "desc" },
442+
},
443+
taskAssignees: {
444+
include: { member: true },
445+
},
446+
project: {
447+
select: { id: true, name: true },
448+
},
449+
},
450+
})
451+
452+
return tasks.map((t) => ({
453+
...t,
454+
createdAt: t.createdAt?.toISOString(),
455+
updatedAt: t.updatedAt?.toISOString(),
456+
comments: (t.comments || []).map((c) => ({
457+
...c,
458+
createdAt: c.createdAt?.toISOString(),
459+
updatedAt: c.updatedAt?.toISOString(),
460+
})),
461+
project: t.project
462+
? {
463+
id: t.project.id,
464+
name: t.project.name,
465+
}
466+
: null,
467+
taskAssignees: t.taskAssignees.map((a) => ({
468+
id: a.member.id,
469+
name: a.member.name,
470+
photo: a.member.photo,
471+
phone: a.member.phone,
472+
role: a.member.role,
473+
})),
474+
}))
475+
}
476+
477+
478+
479+
// Tasks Workspace
480+
async getTasksWorkspace(
481+
workspaceId?: string,
482+
memberId?: string,
483+
startDate?: string,
484+
endDate?: string
485+
) {
486+
487+
const now = new Date();
488+
489+
// default 60 days
490+
const defaultStart = new Date();
491+
defaultStart.setDate(now.getDate() - 60);
492+
493+
const parsedStart = parseDateSafe(startDate);
494+
const parsedEnd = parseDateSafe(endDate);
495+
496+
const start = parsedStart
497+
? startOfDay(parsedStart)
498+
: startOfDay(defaultStart);
499+
500+
const end = parsedEnd
501+
? endOfDay(parsedEnd)
502+
: endOfDay(now);
503+
504+
const safeMemberId =
505+
memberId && memberId !== "undefined" && memberId !== "null"
506+
? memberId
507+
: undefined;
508+
509+
510+
const where: any = {
511+
isTrash: false,
512+
createdAt: {
513+
gte: start,
514+
lte: end,
515+
},
516+
project: {
517+
workspaceId,
518+
},
519+
...(safeMemberId && {
520+
taskAssignees: {
521+
some: { memberId: safeMemberId },
522+
},
523+
}),
524+
};
525+
526+
527+
const tasks = await prisma.task.findMany({
528+
where,
529+
orderBy: [
530+
{
531+
project: {
532+
name: "asc", // urutkan berdasarkan nama project
533+
},
534+
},
535+
{
536+
createdAt: "desc", // lalu task terbaru
537+
},
538+
],
539+
include: {
540+
comments: {
541+
where: { isTrash: false },
542+
orderBy: { createdAt: "desc" },
543+
},
544+
taskAssignees: {
545+
include: {
546+
member: true,
547+
},
548+
},
549+
project: true,
550+
},
551+
});
552+
553+
554+
return tasks.map((t) => ({
555+
...t,
556+
createdAt: t.createdAt?.toISOString(),
557+
updatedAt: t.updatedAt?.toISOString(),
558+
comments: (t.comments || []).map((c) => ({
559+
...c,
560+
createdAt: c.createdAt?.toISOString(),
561+
updatedAt: c.updatedAt?.toISOString(),
562+
})),
563+
taskAssignees: t.taskAssignees.map((a) => ({
348564
id: a.member.id,
349565
name: a.member.name,
350566
photo: a.member.photo,
351567
phone: a.member.phone,
352-
role: a.member.role
353-
}))
568+
role: a.member.role,
569+
})),
354570
}));
355571
}
356572

@@ -360,9 +576,6 @@ export class ProjectManagementService {
360576
const existingCount = await prisma.taskAssignee.count()
361577

362578
if (existingCount > 0) {
363-
console.log(
364-
"[MIGRATION] TaskAssignee already has data. Migration skipped."
365-
)
366579
return { skipped: true, reason: "already_migrated" }
367580
}
368581

@@ -568,6 +781,20 @@ export class ProjectManagementService {
568781
}
569782
}
570783

784+
let finishDateUpdate: Date | null | undefined = undefined;
785+
const nextStatus = payload.status ?? existing.status;
786+
787+
if (nextStatus === "done" && existing.status !== "done") {
788+
// baru saja selesai
789+
finishDateUpdate = new Date();
790+
}
791+
792+
if (nextStatus !== "done" && existing.status === "done") {
793+
// batal selesai
794+
finishDateUpdate = null;
795+
}
796+
797+
571798
// -----------------------------
572799
// UPDATE TASK CORE FIELDS
573800
// -----------------------------
@@ -576,21 +803,32 @@ export class ProjectManagementService {
576803
data: {
577804
title: payload.title ?? existing.title,
578805
description: payload.description ?? existing.description,
806+
579807
projectId:
580808
payload.projectId !== undefined
581809
? payload.projectId
582810
: existing.projectId,
583-
status: payload.status ?? existing.status,
811+
812+
status: nextStatus,
813+
814+
finishDate: finishDateUpdate,
815+
584816
priority: payload.priority ?? existing.priority,
817+
585818
startDate:
586819
payload.startDate !== undefined
587820
? payload.startDate
588821
: existing.startDate,
822+
589823
dueDate:
590-
payload.dueDate !== undefined ? payload.dueDate : existing.dueDate,
824+
payload.dueDate !== undefined
825+
? payload.dueDate
826+
: existing.dueDate,
827+
591828
updatedAt: new Date(),
592829
},
593-
})
830+
});
831+
594832

595833
// -----------------------------
596834
// SYNC TASK ASSIGNEES (REPLACE)

0 commit comments

Comments
 (0)