Skip to content

Commit

Permalink
feat: daily summary report (#207)
Browse files Browse the repository at this point in the history
  • Loading branch information
hudy9x authored Jun 11, 2024
1 parent eae739b commit 9b3e8f8
Show file tree
Hide file tree
Showing 25 changed files with 786 additions and 201 deletions.
57 changes: 57 additions & 0 deletions DOCUMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,63 @@ export const useEventDeleteComment = () => {
}
```

## Create a task scheduler

To run a task in schedule, for example: run a task per 1h, run a task per Monday at 20h
Do the following steps:

### Step 1 - Create an event in backend
Open `packages/be-gateway/src/events/index.ts` then create an event name and add a handler to it.

```typescript

export const CHANNEL_DAY_STATS = 'stats:day-stats'

// We must subscribe channels first
redis.subscribe(CHANNEL_DAY_STATS)

// After that, we can listen messages from them
redis.on('message', async (channel: string, data: string) => {
if (channel === CHANNEL_DAY_STATS) {
const dayStats = new StatsByDayEvent()
dayStats.run()
}
})
```

Next, create the event handler at `packages/be-gateway/src/events/` folder. Ex: `stats.day.event.ts`
```typescript
export default class StatsByDayEvent {
constructor() {

}
async run() {

}
}
```

### Step 2 - Publish to the above event
After registering event we need to publish message to trigger it. Open `packages/be-scheduler/src/main.ts` and create a cronjob as follows

```typescript
connectPubClient((err, redis) => {
if (err) return

// run every 20pm
const runAt20h = 'runAt20pm'
cronJob.create(runAt20h, '5 12,20 * * *', () => {

// Remember that, channel name must be same as Event name
const CHANNEL_DAY_STATS = 'stats:day-stats'
redis.publish(CHANNEL_DAY_STATS, 'heelo')
})
})

```



## Configure environment variables
### Required configs
|Name|Value|Desc|Required|
Expand Down
14 changes: 10 additions & 4 deletions packages/be-gateway/src/events/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { connectSubClient } from '@shared/pubsub'
import { NotificationEvent } from './notification.event'
import { ReminderEvent } from './reminder.event'
import StatsByDayEvent from './stats.day.event'

export const CHANNEL_SCHEDULER_ACTION_NOTIFY = 'scheduler:action-notify'
export const CHANNEL_SCHEDULER_CREATE = 'scheduler:create'
export const CHANNEL_RUN_EVERY_MINUTE = 'fixed:run-every-minute'
export const CHANNEL_DAY_STATS = 'stats:day-stats'
export const EVENT = {
SCHEDULER_DELETE: 'scheduler:delete'
}
Expand All @@ -14,14 +16,13 @@ connectSubClient((err, redis) => {
return
}
// We must subscribe channels first
redis.subscribe(CHANNEL_SCHEDULER_ACTION_NOTIFY, (err, count) => {
console.log('subscribed', CHANNEL_SCHEDULER_ACTION_NOTIFY)
})

redis.subscribe(CHANNEL_SCHEDULER_ACTION_NOTIFY)
redis.subscribe(CHANNEL_RUN_EVERY_MINUTE)
redis.subscribe(CHANNEL_DAY_STATS)

// After that, we can listen messages from them
redis.on('message', async (channel: string, data: string) => {
console.log('channel:', channel)
if (channel === CHANNEL_SCHEDULER_ACTION_NOTIFY) {
const event = new NotificationEvent()
event.run(data)
Expand All @@ -30,5 +31,10 @@ connectSubClient((err, redis) => {
const reminder = new ReminderEvent()
reminder.run()
}

if (channel === CHANNEL_DAY_STATS) {
const dayStats = new StatsByDayEvent()
dayStats.run()
}
})
})
31 changes: 31 additions & 0 deletions packages/be-gateway/src/events/stats.day.event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import ProjectRepository from "packages/shared-models/src/lib/project.repository";
import { StatsQueue, getStatsQueueInstance } from "../queues/Stats";

export default class StatsByDayEvent {
projectRepo: ProjectRepository
statsQueue: StatsQueue
constructor() {
this.statsQueue = getStatsQueueInstance()
this.projectRepo = new ProjectRepository()
}
async run() {
const { projectsWMemberEnabled, projectsWCounterEnabled } = await this.projectRepo.getProjectsWithCountSettingEnabled()

console.log('stats.day.event called', new Date())
if (!projectsWCounterEnabled || !projectsWCounterEnabled.length) {
console.log('No project with project counter enabled')
}

projectsWCounterEnabled.map(pid => {
this.statsQueue.addJob('unDoneTasksByProject', pid)
})

if (!projectsWMemberEnabled || !projectsWMemberEnabled.length) {
console.log('No project with member counter enabled')
}
projectsWMemberEnabled.map(pid => {
this.statsQueue.addJob('doneTasksByMember', pid)
})

}
}
14 changes: 14 additions & 0 deletions packages/be-gateway/src/lib/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,17 @@ export const createModuleLog = (module: string) => {
})
}

export const sendDiscordLog = async (content: string) => {
const data = {
username: 'Scheduler',
avatar_url: "",
content
}
return fetch("https://discord.com/api/webhooks/1249577190626955284/QWVUtgJVOj6JVqlRb7qyZ-MoIKYRUhUm94hXLxXPMi3a23XSmlGfeyPo40x7hHPmlEts", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data)
})
}
16 changes: 16 additions & 0 deletions packages/be-gateway/src/queues/Stats/DoneTasksByMemberJob.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

import StatsDoneTaskService from '../../services/stats/done.tasks.service'
import { BaseJob } from '../BaseJob'


export class DoneTasksByMemberJob extends BaseJob {
name = 'doneTasksByMember'
service: StatsDoneTaskService
constructor() {
super()
this.service = new StatsDoneTaskService()
}
async implement(projectId: string) {
await this.service.implement(projectId)
}
}
16 changes: 16 additions & 0 deletions packages/be-gateway/src/queues/Stats/UnDoneTasksByProjectJob.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

import StatsUnDoneTaskService from '../../services/stats/undone.tasks.service'
import { BaseJob } from '../BaseJob'


export class UnDoneTasksByProjectJob extends BaseJob {
name = 'unDoneTasksByProject'
service: StatsUnDoneTaskService
constructor() {
super()
this.service = new StatsUnDoneTaskService()
}
async implement(projectId: string) {
await this.service.implement(projectId)
}
}
24 changes: 24 additions & 0 deletions packages/be-gateway/src/queues/Stats/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { BaseQueue } from '../BaseQueue'
import { DoneTasksByMemberJob } from './DoneTasksByMemberJob'
import { UnDoneTasksByProjectJob } from './UnDoneTasksByProjectJob'


export class StatsQueue extends BaseQueue {
constructor() {
super()
this.queueName = 'Stats'
this.jobs = [new DoneTasksByMemberJob(), new UnDoneTasksByProjectJob()]

this.run()
}
}

let instance: StatsQueue = null

export const getStatsQueueInstance = () => {
if (!instance) {
instance = new StatsQueue()
}

return instance
}
14 changes: 14 additions & 0 deletions packages/be-gateway/src/services/activity.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default class ActivityService {
assigneeIds,
priority,
taskPoint,
startDate,
dueDate,
progress,
fileIds,
Expand Down Expand Up @@ -140,6 +141,19 @@ export default class ActivityService {
}
}

if (startDate) {
if (!isSameDay(new Date(taskData.startDate), new Date(startDate))) {
console.log('startDate changed')
const newActivity = structuredClone(activityTemplate)
newActivity.type = ActivityType.TASK_DUEDATE_CHANGED
newActivity.data = {
changeFrom: new Date(taskData.startDate).toISOString(),
changeTo: new Date(startDate).toISOString()
}
updatingActivities.push(newActivity)
}
}

if (dueDate) {
if (!isSameDay(new Date(taskData.dueDate), new Date(dueDate))) {
console.log('dueDate changed')
Expand Down
14 changes: 14 additions & 0 deletions packages/be-gateway/src/services/project/index.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import ProjectRepository from "packages/shared-models/src/lib/project.repository";

export default class ProjectService {
projectRepo: ProjectRepository
constructor() {
this.projectRepo = new ProjectRepository()
}
async getAllProjectIds() {
const projectIds = await this.projectRepo.getAvailableProjectIds()

return projectIds
}

}
134 changes: 134 additions & 0 deletions packages/be-gateway/src/services/stats/done.tasks.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { StatsType, StatusType } from "@prisma/client";
import { lastDayOfMonth } from "date-fns";
import { pmClient } from "packages/shared-models/src/lib/_prisma";
import { sendDiscordLog } from "../../lib/log";

export default class StatsDoneTaskService {
async implement(projectId: string) {

try {
const doneStatus = await pmClient.taskStatus.findMany({
where: {
type: StatusType.DONE,
},
select: {
id: true,
}
})

const ids = doneStatus.map(d => d.id)

const now = new Date()
const y = now.getFullYear()
const m = now.getMonth()
const d = now.getDate()
const month = m + 1

const firstDay = new Date(y, m, 1, 0, 0)
const lastDay = lastDayOfMonth(now)
lastDay.setHours(23)
lastDay.setMinutes(59)

const result = await pmClient.task.findMany({
where: {
projectId,
assigneeIds: {
isEmpty: false
},
taskStatusId: {
in: ids
},
OR: [
{
AND: [
{
dueDate: {
gte: firstDay
}
},
{
dueDate: {
lte: lastDay
}
},

]

}
]
},
select: {
id: true,
assigneeIds: true,
dueDate: true,
}
})


const totalByMembers = new Map<string, number>()

result.forEach(r => {
r.assigneeIds.forEach(a => {
if (totalByMembers.has(a)) {
totalByMembers.set(a, totalByMembers.get(a) + 1)
} else {
totalByMembers.set(a, 1)
}
})

})

totalByMembers.forEach(async (total, uid) => {

const existing = await pmClient.stats.findFirst({
where: {
projectId,
type: StatsType.MEMBER_TASK_BY_DAY,
uid,
year: y,
month,
date: d
}
})

// create new if doesn't exist
if (!existing) {
await pmClient.stats.create({
data: {
type: StatsType.MEMBER_TASK_BY_DAY,
projectId,
uid,
year: y,
month,
date: d,
data: {
doneTotal: total
},
updatedAt: new Date()
}
})

// update if existing
} else {
await pmClient.stats.update({
where: {
id: existing.id
},
data: {
data: {
doneTotal: total
},
updatedAt: new Date()
}

})
}
})

sendDiscordLog("Count done tasks per member finished")
} catch (error) {
sendDiscordLog("done.task.service.error: " + JSON.stringify(error))
console.log('done.task.service error', error)
}
}
}
Loading

0 comments on commit 9b3e8f8

Please sign in to comment.