[ENHANCEMENT] [ACTIVITY-SUMMARY] Improvisations post-hacktoberfest (#15882)
## Background Team Comfortably Summed had submitted Activity Summary for Twenty's Hacktoberfest. This is a follow-up PR post-Hacktoberfest. ## Changes ### Purpose >_What is the objective?_ To improve the code based on human and bot comments from the initial Hacktoberfest PR, and to improve the robustness of task completion calculation. >_Are we solving a problem or making necessary changes to support an existing and / or a new feature?_ Making changes to improve existing features. ### Summary > _Are there files we can ignore?_ None. > _Please provide a high-level summary of the changes._ - Corrects `README.md` by removing irrelevant commands and unnecessary configuration notes - Updates `people-creation-summariser.ts` - Replaces requesting of company for each person by including `depth=1` as query parameter in GET /people request as suggested by Martin Muller – link to comment: [link](https://github.com/twentyhq/twenty/pull/15510#discussion_r2486019035) - Updates `senders.ts` - Renames `formattedMesage` to `formattedMessage` - Updates `task-creation-summariser.ts` - Improves calculation of completed task percentage by relying only on list of completed tasks and total list of tasks - The initial implementation includes relying on pending tasks and pending overdue tasks which can result in undesired calculation outcome
This commit is contained in:
@@ -20,29 +20,10 @@ A TypeScript-based reporting bot that summarizes activity from your Twenty CRM w
|
||||
- A [Twenty CRM](https://twenty.com) account with API access
|
||||
- Optional: Slack webhook, Discord webhook, and/or WhatsApp Business API access
|
||||
|
||||
## Installation
|
||||
## Installing dependencies
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone <your-repo-url>
|
||||
cd <project-directory>
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Create a `.env` file in the root directory with the following variables:
|
||||
```bash
|
||||
# Required
|
||||
TWENTY_API_KEY=your_twenty_api_key_here
|
||||
DAYS_AGO=7 # Number of days to look back
|
||||
|
||||
# Optional - Include only the platforms you want to use
|
||||
SLACK_HOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
|
||||
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/YOUR/WEBHOOK/URL
|
||||
FB_GRAPH_TOKEN=your_facebook_graph_api_token
|
||||
WHATSAPP_RECIPIENT_PHONE_NUMBER=+1234567890
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
@@ -56,17 +37,6 @@ WHATSAPP_RECIPIENT_PHONE_NUMBER=+1234567890
|
||||
| `FB_GRAPH_TOKEN` | ❌ No | Facebook Graph API token for WhatsApp |
|
||||
| `WHATSAPP_RECIPIENT_PHONE_NUMBER` | ❌ No | WhatsApp recipient phone number (with country code) |
|
||||
|
||||
## Usage
|
||||
```bash
|
||||
# Run the application
|
||||
npm start
|
||||
# or
|
||||
node dist/index.js # if compiled
|
||||
|
||||
# For development
|
||||
npm run dev # if you have ts-node configured
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
.
|
||||
@@ -112,23 +82,10 @@ Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last X days:
|
||||
This bot uses the [Twenty CRM REST API](https://api.twenty.com/rest/). The following endpoints are used:
|
||||
|
||||
- `GET /people` - Fetch people data
|
||||
- `GET /companies` - Fetch company data
|
||||
- `GET /opportunities` - Fetch opportunity data
|
||||
- `GET /tasks` - Fetch task data
|
||||
- `GET /workspaceMembers/{id}` - Fetch workspace member details
|
||||
|
||||
## Development
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Compile TypeScript
|
||||
npx tsc
|
||||
|
||||
# Run in development mode
|
||||
npx ts-node index.ts
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The "slacker" detection is lighthearted and identifies team members with the most overdue tasks
|
||||
|
||||
+4
-6
@@ -2,6 +2,7 @@ import { request } from "./utils"
|
||||
|
||||
type Person = {
|
||||
companyId: string
|
||||
company: Company
|
||||
}
|
||||
|
||||
type Company = {
|
||||
@@ -11,7 +12,7 @@ type Company = {
|
||||
|
||||
export const summarisePeopleCreation = async (date: string) => {
|
||||
const { people }: { people: Person[] } = await request(
|
||||
`people?filter=createdAt[gte]:${date}`,
|
||||
`people?depth=1&filter=createdAt[gte]:${date}`,
|
||||
)
|
||||
|
||||
if (people.length === 0) {
|
||||
@@ -24,12 +25,9 @@ export const summarisePeopleCreation = async (date: string) => {
|
||||
for (const person of people) {
|
||||
const isCompanyTracked = createdForCompanies[person.companyId]
|
||||
if (person.companyId && !isCompanyTracked) {
|
||||
const { company }: { company: Company } = await request(
|
||||
`companies/${person.companyId}`,
|
||||
)
|
||||
createdForCompanies[company.id] = company
|
||||
createdForCompanies[person.company.id] = person.company
|
||||
|
||||
if (!company.accountOwnerId) {
|
||||
if (!person?.company?.accountOwnerId) {
|
||||
numberOfAccountOwnerlessCompanies += 1
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -77,7 +77,7 @@ export const sendToSlack = async (params: {
|
||||
});
|
||||
|
||||
return {
|
||||
formattedMesage: slackMessage,
|
||||
formattedMessage: slackMessage,
|
||||
webhookStatus: response.status,
|
||||
};
|
||||
};
|
||||
@@ -92,7 +92,7 @@ export const sendToDiscord = async (params: {
|
||||
opportunityCreationSummary,
|
||||
taskCreationSummary,
|
||||
} = params;
|
||||
const formattedMesage = `Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last ${process.env.DAYS_AGO} days:
|
||||
const formattedMessage = `Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last ${process.env.DAYS_AGO} days:
|
||||
|
||||
**🧑💻 People & Companies**
|
||||
${peopleCreationSummary}
|
||||
@@ -105,7 +105,7 @@ ${taskCreationSummary}`;
|
||||
|
||||
const body = {
|
||||
username: 'Twenty Bot',
|
||||
content: formattedMesage,
|
||||
content: formattedMessage,
|
||||
};
|
||||
|
||||
const response = await fetch(process.env.DISCORD_WEBHOOK_URL ?? '', {
|
||||
@@ -115,7 +115,7 @@ ${taskCreationSummary}`;
|
||||
});
|
||||
|
||||
return {
|
||||
formattedMesage,
|
||||
formattedMessage,
|
||||
webhookStatus: response.status,
|
||||
};
|
||||
};
|
||||
@@ -130,7 +130,7 @@ export const sendToWhatsApp = async (params: {
|
||||
opportunityCreationSummary,
|
||||
taskCreationSummary,
|
||||
} = params;
|
||||
const formattedMesage = `Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last ${process.env.DAYS_AGO} days:
|
||||
const formattedMessage = `Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last ${process.env.DAYS_AGO} days:
|
||||
|
||||
*🧑💻 People & Companies*
|
||||
${peopleCreationSummary}
|
||||
@@ -156,7 +156,7 @@ ${taskCreationSummary}`;
|
||||
type: 'text',
|
||||
text: {
|
||||
preview_url: true,
|
||||
body: formattedMesage,
|
||||
body: formattedMessage,
|
||||
},
|
||||
}),
|
||||
},
|
||||
@@ -165,7 +165,7 @@ ${taskCreationSummary}`;
|
||||
const responseBody = await response.json();
|
||||
|
||||
return {
|
||||
formattedMesage,
|
||||
formattedMessage,
|
||||
webhookStatus: response.status,
|
||||
webhookResponse: responseBody,
|
||||
};
|
||||
|
||||
+16
-10
@@ -16,26 +16,23 @@ export const summariseTaskCreation = async (date: string) => {
|
||||
return '- No Tasks were added'
|
||||
}
|
||||
|
||||
const presentDateISOString = new Date().toISOString()
|
||||
const workspaceMemberIdsDueDateCounter: Record<string, number> = {}
|
||||
let workspaceMembers = []
|
||||
const completedTasks: Task[] = []
|
||||
|
||||
const pendingTasksWithPastDueDates = tasks.reduce((collection: Task[], task) => {
|
||||
tasks.forEach((task) => {
|
||||
if (task.status === 'DONE') {
|
||||
completedTasks.push(task)
|
||||
return collection
|
||||
return
|
||||
}
|
||||
|
||||
if (new Date().toISOString() > task.dueAt) {
|
||||
if (presentDateISOString >= task.dueAt) {
|
||||
if (!workspaceMemberIdsDueDateCounter[task.createdBy.workspaceMemberId]) {
|
||||
workspaceMemberIdsDueDateCounter[task.createdBy.workspaceMemberId] = 0
|
||||
}
|
||||
|
||||
workspaceMemberIdsDueDateCounter[task.createdBy.workspaceMemberId] += 1
|
||||
collection.push(task)
|
||||
}
|
||||
return collection
|
||||
}, [])
|
||||
})
|
||||
|
||||
for (const { userId, count } of findMaxIncompleteKeys(workspaceMemberIdsDueDateCounter)) {
|
||||
const data = await request(`workspaceMembers/${userId}`)
|
||||
@@ -61,8 +58,17 @@ export const summariseTaskCreation = async (date: string) => {
|
||||
}
|
||||
}, '') : 'No one was caught slacking!'
|
||||
|
||||
const taskCompletionPercentage = pendingTasksWithPastDueDates.length > 0 ? 100 - (100 - ((pendingTasksWithPastDueDates.length / (tasks.length - completedTasks.length)) * 100)) : NaN
|
||||
const taskCompletionMessage = isNaN(taskCompletionPercentage) ? 'No Tasks to be completed' : `${taskCompletionPercentage.toFixed(2)}% Tasks were completed on time`
|
||||
const tasksCompletedOnTime = tasks.filter(
|
||||
task => task.status === 'DONE' && task.dueAt >= presentDateISOString
|
||||
)
|
||||
|
||||
const taskCompletionPercentage = tasksCompletedOnTime.length > 0
|
||||
? (tasksCompletedOnTime.length / tasks.length) * 100
|
||||
: NaN
|
||||
|
||||
const taskCompletionMessage = isNaN(taskCompletionPercentage)
|
||||
? 'No completed Tasks yet'
|
||||
: `${taskCompletionPercentage.toFixed(2)}% of Tasks were completed on time`
|
||||
|
||||
return `- ${tasks.length} Tasks were created
|
||||
- ${taskCompletionMessage}
|
||||
|
||||
Reference in New Issue
Block a user