[HACKTOBERFEST] [FEATURE] Create activity summary application (#15510)
## Background This is team Comfortably Summed's submission for Twenty's Hacktoberfest. We built an activity summary application that can periodically send messages to the following platforms: Slack; Discord; and WhatsApp. ### Features - 🧑💻 **People & Company Tracking**: Summarizes newly created people and companies - 🎯 **Opportunity Monitoring**: Reports on new opportunities created, broken down by stage - ✅ **Task Analytics**: - Tracks task creation - Calculates on-time completion rates - Identifies team members with the most overdue tasks (the "slackers") - 🔔 **Multi-Platform Notifications**: Send reports to Slack, Discord, and/or WhatsApp - ⏰ **Configurable Time Range**: Look back any number of days ### Summary of Changes - Adds a new Twenty app called Activity Summary - Contains a single index.ts file which utilises exported functions from opportunity-creation-summariser.ts, people-creation-summariser.ts, task-creation-summariser.ts, and senders.ts - Implementation of sending a message to Slack, Discord, and WhatsApp can be found in senders.ts - Retrieval and summarising of Opportunity creation can be found in opportunity-creation-summariser.ts - Retrieval and summarising of People creation can be found in people-creation-summariser.ts - Retrieval and summarising of Task creation can be found in task-creation-summariser.ts ## Screenshots ### Message to our Slack channel <details> <summary>Screenshot</summary> <img width="326" height="242" alt="Screenshot 2025-11-01 at 22 05 30" src="https://github.com/user-attachments/assets/57c5d50b-959d-4c3f-bd7d-00f42bf545b2" /> </details> ### Message to our Discord server's channel <details> <summary>Screenshot</summary> <img width="472" height="386" alt="Screenshot 2025-11-01 at 22 06 44" src="https://github.com/user-attachments/assets/f4a38d7f-e82d-47b0-a4b3-7bcf063fa575" /> </details> ### Message to our WhatsApp number <details> <summary>Screenshot</summary> <img width="972" height="548" alt="IMG_2024" src="https://github.com/user-attachments/assets/5533fc4d-a3ee-4e11-a9e7-9cc6a96316fc" /> </details> ### App-level configuration <details> <summary>Screenshot</summary> <img width="442" height="385" alt="Screenshot 2025-11-01 at 22 02 14" src="https://github.com/user-attachments/assets/c9948f57-f22c-42a0-972f-3348f480aa30" /> </details> ### Serverless functions <details> <summary>Screenshot</summary> <img width="413" height="378" alt="Screenshot 2025-11-01 at 22 03 48" src="https://github.com/user-attachments/assets/d297967b-52ce-4690-bb04-a16d89729d94" /> </details> ### Cron configuration Default value in place due to Cron having a non-editable text input. <details> <summary>Screenshot</summary> <img width="395" height="386" alt="Screenshot 2025-11-01 at 22 08 03" src="https://github.com/user-attachments/assets/a95a708c-7136-4512-99c3-a6723adc0da5" /> </details> ## Testing Sync the application to your Twenty instance and ensure the following variables have values: - `TWENTY_API_KEY` - Your Twenty CRM API key - `DAYS_AGO` - Number of days to look back for the report Choose any of the supported platforms and you shall see a summary being sent!
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
TWENTY_API_KEY=<SET_YOUR_TWENTY_API>
|
||||
DAYS_AGO=<SET_YOUR_DAYS_AGO>
|
||||
DISCORD_WEBHOOK_URL=<SET_YOUR_DISCORD_WEBHOOK_URL>
|
||||
FB_GRAPH_TOKEN=<SET_YOUR_FB_GRAPH_TOKEN>
|
||||
WHATSAPP_RECIPIENT_PHONE_NUMBER=<SET_YOUR_WHATSAPP_RECIPIENT_PHONE_NUMBER>
|
||||
SLACK_HOOK_URL=<SET_YOUR_SLACK_HOOK_URL>
|
||||
@@ -0,0 +1,2 @@
|
||||
.yarn/install-state.gz
|
||||
.env
|
||||
+942
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
||||
yarnPath: .yarn/releases/yarn-4.9.2.cjs
|
||||
|
||||
nodeLinker: node-modules
|
||||
@@ -0,0 +1,151 @@
|
||||
# Twenty CRM Activity Reporter ��
|
||||
|
||||
A TypeScript-based reporting bot that summarizes activity from your Twenty CRM workspace and sends daily/periodic reports to Slack, Discord, and WhatsApp. Meet Kylian Mbaguette, your friendly CRM activity reporter!
|
||||
|
||||
## Features
|
||||
|
||||
- 🧑💻 **People & Company Tracking**: Summarizes newly created people and companies
|
||||
- 🎯 **Opportunity Monitoring**: Reports on new opportunities created, broken down by stage
|
||||
- ✅ **Task Analytics**:
|
||||
- Tracks task creation
|
||||
- Calculates on-time completion rates
|
||||
- Identifies team members with the most overdue tasks (the "slackers")
|
||||
- 🔔 **Multi-Platform Notifications**: Send reports to Slack, Discord, and/or WhatsApp
|
||||
- ⏰ **Configurable Time Range**: Look back any number of days
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js (v14 or higher recommended)
|
||||
- TypeScript
|
||||
- A [Twenty CRM](https://twenty.com) account with API access
|
||||
- Optional: Slack webhook, Discord webhook, and/or WhatsApp Business API access
|
||||
|
||||
## Installation
|
||||
```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
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `TWENTY_API_KEY` | ✅ Yes | Your Twenty CRM API key |
|
||||
| `DAYS_AGO` | ✅ Yes | Number of days to look back for the report |
|
||||
| `SLACK_HOOK_URL` | ❌ No | Slack incoming webhook URL |
|
||||
| `DISCORD_WEBHOOK_URL` | ❌ No | Discord webhook URL |
|
||||
| `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
|
||||
```
|
||||
.
|
||||
├── index.ts # Main entry point
|
||||
├── people-creation-summariser.ts # Summarizes people/company creation
|
||||
├── opportunity-creation-summariser.ts # Summarizes opportunity creation
|
||||
├── task-creation-summariser.ts # Summarizes task creation & completion
|
||||
├── senders.ts # Handles sending to Slack/Discord/WhatsApp
|
||||
├── utils.ts # API request utility
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Data Collection**: The bot queries the Twenty CRM API for activities within the specified time range
|
||||
2. **Analysis**:
|
||||
- Counts new people and companies
|
||||
- Categorizes opportunities by stage
|
||||
- Calculates task completion rates and identifies overdue tasks
|
||||
3. **Reporting**: Formats the data into friendly messages
|
||||
4. **Distribution**: Sends reports to configured platforms (Slack, Discord, WhatsApp)
|
||||
|
||||
## Report Format
|
||||
|
||||
Each report includes:
|
||||
```
|
||||
Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last X days:
|
||||
|
||||
🧑💻 People & Companies
|
||||
- X People and Y Companies were added
|
||||
|
||||
🎯 Opportunities
|
||||
- X Opportunities were added: Y in NEW, Z in PROPOSAL
|
||||
|
||||
📋 Tasks
|
||||
- X Tasks were created
|
||||
- Y% Tasks were completed on time
|
||||
- [Name] slacked the most with Z Tasks overdue
|
||||
```
|
||||
|
||||
## API Integration
|
||||
|
||||
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
|
||||
- At least one messaging platform must be configured for the bot to send reports
|
||||
- The bot uses ISO date format (YYYY-MM-DD) for date filtering
|
||||
- Task completion percentage only considers incomplete tasks (excludes already completed tasks from the calculation)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue**: No messages being sent
|
||||
- **Solution**: Ensure at least one messaging platform is configured with valid credentials
|
||||
|
||||
**Issue**: API authentication errors
|
||||
- **Solution**: Verify your `TWENTY_API_KEY` is correct and has necessary permissions
|
||||
|
||||
**Issue**: WhatsApp messages not sending
|
||||
- **Solution**: Ensure both `FB_GRAPH_TOKEN` and `WHATSAPP_RECIPIENT_PHONE_NUMBER` are set correctly
|
||||
|
||||
## Contributing
|
||||
Built with ❤️ and 🥖 by Azmat, Ali and Mike from 9dots
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"npm": "please-use-yarn",
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"dependencies": {
|
||||
"twenty-sdk": "0.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2"
|
||||
},
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/appManifest.schema.json",
|
||||
"universalIdentifier": "b53627f5-ca60-478c-bc43-c7ab4904e34a",
|
||||
"name": "Activity Summary",
|
||||
"description": "A TypeScript-based reporting bot that summarizes activity from your Twenty CRM workspace and sends daily/periodic reports to Slack, Discord, and WhatsApp. Meet Kylian Mbaguette, your friendly CRM activity reporter!",
|
||||
"env": {
|
||||
"TWENTY_API_KEY": {
|
||||
"description": "Twenty API Key",
|
||||
"isSecret": true
|
||||
},
|
||||
"DAYS_AGO": {
|
||||
"description": "How far back into the past we want to summarise – defaults to the past 7 days",
|
||||
"value": "7",
|
||||
"isSecret": false
|
||||
},
|
||||
"SLACK_HOOK_URL": {
|
||||
"description": "Slack hook URL for sending message to channel",
|
||||
"isSecret": true
|
||||
},
|
||||
"DISCORD_WEBHOOK_URL": {
|
||||
"description": "Discord webhook URL for sending message to channel of a server",
|
||||
"isSecret": true
|
||||
},
|
||||
"FB_GRAPH_TOKEN": {
|
||||
"description": "For Facebook auth",
|
||||
"isSecret": true
|
||||
},
|
||||
"WHATSAPP_RECIPIENT_PHONE_NUMBER": {
|
||||
"description": "Phone number for receiving WhatsApp message",
|
||||
"isSecret": true
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/serverlessFunction.schema.json",
|
||||
"universalIdentifier": "c5b0e3f7-cbbd-4bd6-b01c-150d52cf2ce9",
|
||||
"name": "summarise-and-send",
|
||||
"triggers": [
|
||||
{
|
||||
"universalIdentifier": "36e1c4c7-8664-4d6d-a88f-ac56f1bd0651",
|
||||
"type": "cron",
|
||||
"schedule": "0 9 * * *"
|
||||
}
|
||||
]
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { summariseOpportunityCreation } from "./opportunity-creation-summariser";
|
||||
import { summarisePeopleCreation } from "./people-creation-summariser";
|
||||
import { sendToDiscord, sendToSlack, sendToWhatsApp } from "./senders";
|
||||
import { summariseTaskCreation } from "./task-creation-summariser";
|
||||
|
||||
export const main = async (): Promise<object> => {
|
||||
let date: string | Date = new Date()
|
||||
date.setDate(new Date().getDate() - Number(process.env.DAYS_AGO))
|
||||
date = date.toISOString().substring(0, 10)
|
||||
const peopleCreationSummary = await summarisePeopleCreation(date)
|
||||
const opportunityCreationSummary = await summariseOpportunityCreation(date)
|
||||
const taskCreationSummary = await summariseTaskCreation(date)
|
||||
|
||||
let body = {
|
||||
daysAgo: Number(process.env.DAYS_AGO),
|
||||
peopleCreationSummary,
|
||||
opportunityCreationSummary,
|
||||
taskCreationSummary,
|
||||
discord: {},
|
||||
whatsapp: {},
|
||||
slack: {},
|
||||
}
|
||||
|
||||
if (process.env.SLACK_HOOK_URL) {
|
||||
const slackBody = await sendToSlack({
|
||||
peopleCreationSummary,
|
||||
opportunityCreationSummary,
|
||||
taskCreationSummary,
|
||||
})
|
||||
|
||||
body = {
|
||||
...body,
|
||||
slack: slackBody,
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.DISCORD_WEBHOOK_URL) {
|
||||
const discordBody = await sendToDiscord({
|
||||
peopleCreationSummary,
|
||||
opportunityCreationSummary,
|
||||
taskCreationSummary,
|
||||
})
|
||||
|
||||
body = {
|
||||
...body,
|
||||
discord: discordBody,
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.FB_GRAPH_TOKEN && process.env.WHATSAPP_RECIPIENT_PHONE_NUMBER) {
|
||||
const whatsappBody = await sendToWhatsApp({
|
||||
peopleCreationSummary,
|
||||
opportunityCreationSummary,
|
||||
taskCreationSummary,
|
||||
})
|
||||
|
||||
body = {
|
||||
...body,
|
||||
whatsapp: whatsappBody,
|
||||
}
|
||||
}
|
||||
|
||||
return body
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { request } from "./utils"
|
||||
|
||||
type Opportunity = {
|
||||
stage: 'NEW' | 'PROPOSAL'
|
||||
}
|
||||
|
||||
export const summariseOpportunityCreation = async (date: string) => {
|
||||
const { opportunities }: { opportunities: Opportunity[] } = await request(
|
||||
`opportunities?filter=createdAt[gte]:${date}`,
|
||||
)
|
||||
|
||||
if (opportunities.length === 0) {
|
||||
return `- No Opportunities were added`
|
||||
}
|
||||
|
||||
const stageSummary = Object.entries(
|
||||
opportunities.reduce((hash: Record<string,number>, opportunity) => {
|
||||
if (!hash[opportunity.stage]) {
|
||||
hash[opportunity.stage] = 0
|
||||
}
|
||||
|
||||
hash[opportunity.stage] += 1
|
||||
return hash
|
||||
}, {})
|
||||
).map(([stage, value]) => `${value} in ${stage}`).join(', ')
|
||||
|
||||
return `- ${opportunities.length} Opportunities were added: ${stageSummary}`
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { request } from "./utils"
|
||||
|
||||
type Person = {
|
||||
companyId: string
|
||||
}
|
||||
|
||||
type Company = {
|
||||
id: string
|
||||
accountOwnerId: string
|
||||
}
|
||||
|
||||
export const summarisePeopleCreation = async (date: string) => {
|
||||
const { people }: { people: Person[] } = await request(
|
||||
`people?filter=createdAt[gte]:${date}`,
|
||||
)
|
||||
|
||||
if (people.length === 0) {
|
||||
return '- No People were added'
|
||||
}
|
||||
|
||||
let createdForCompanies: Record<string, Company> = {}
|
||||
let numberOfAccountOwnerlessCompanies = 0
|
||||
|
||||
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
|
||||
|
||||
if (!company.accountOwnerId) {
|
||||
numberOfAccountOwnerlessCompanies += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return `- ${people.length} People were added for ${Object.keys(createdForCompanies).length} Companies
|
||||
- Out of those, ${numberOfAccountOwnerlessCompanies} Companies don't have account owners yet`
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
export const sendToSlack = async (params: {
|
||||
peopleCreationSummary: string
|
||||
opportunityCreationSummary: string
|
||||
taskCreationSummary: string
|
||||
}) => {
|
||||
const { peopleCreationSummary, opportunityCreationSummary, taskCreationSummary } = params
|
||||
|
||||
const slackMessage = {
|
||||
"blocks": [
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": `Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last ${process.env.DAYS_AGO} days`,
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "🧑💻 People & Companies",
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": peopleCreationSummary,
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "🎯 Opportunities",
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": opportunityCreationSummary,
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "📋 Tasks",
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": taskCreationSummary,
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const response = await fetch(process.env.SLACK_HOOK_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(slackMessage),
|
||||
})
|
||||
|
||||
return {
|
||||
formattedMesage: slackMessage,
|
||||
webhookStatus: response.status,
|
||||
}
|
||||
}
|
||||
|
||||
export const sendToDiscord = async (params: {
|
||||
peopleCreationSummary: string
|
||||
opportunityCreationSummary: string
|
||||
taskCreationSummary: string
|
||||
}) => {
|
||||
const {
|
||||
peopleCreationSummary,
|
||||
opportunityCreationSummary,
|
||||
taskCreationSummary,
|
||||
} = params
|
||||
const formattedMesage = `Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last ${process.env.DAYS_AGO} days:
|
||||
|
||||
**🧑💻 People & Companies**
|
||||
${peopleCreationSummary}
|
||||
|
||||
**🎯 Opportunities**
|
||||
${opportunityCreationSummary}
|
||||
|
||||
**📋 Tasks**
|
||||
${taskCreationSummary}`
|
||||
|
||||
|
||||
const body = {
|
||||
username: "Twenty Bot",
|
||||
content: formattedMesage,
|
||||
}
|
||||
|
||||
const response = await fetch(process.env.DISCORD_WEBHOOK_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
return {
|
||||
formattedMesage,
|
||||
webhookStatus: response.status,
|
||||
}
|
||||
}
|
||||
|
||||
export const sendToWhatsApp = async (params: {
|
||||
peopleCreationSummary: string
|
||||
opportunityCreationSummary: string
|
||||
taskCreationSummary: string
|
||||
}): Promise<object> => {
|
||||
const {
|
||||
peopleCreationSummary,
|
||||
opportunityCreationSummary,
|
||||
taskCreationSummary,
|
||||
} = params
|
||||
const formattedMesage = `Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last ${process.env.DAYS_AGO} days:
|
||||
|
||||
*🧑💻 People & Companies*
|
||||
${peopleCreationSummary}
|
||||
|
||||
*🎯 Opportunities*
|
||||
${opportunityCreationSummary}
|
||||
|
||||
*📋 Tasks*
|
||||
${taskCreationSummary}`
|
||||
|
||||
const response = await fetch('https://graph.facebook.com/v22.0/828771160324576/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${process.env.FB_GRAPH_TOKEN}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"messaging_product": "whatsapp",
|
||||
"recipient_type": "individual",
|
||||
"to": process.env.WHATSAPP_RECIPIENT_PHONE_NUMBER,
|
||||
"type": "text",
|
||||
"text": {
|
||||
"preview_url": true,
|
||||
"body": formattedMesage,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const responseBody = await response.json()
|
||||
|
||||
return {
|
||||
formattedMesage,
|
||||
webhookStatus: response.status,
|
||||
webhookResponse: responseBody,
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import { request } from "./utils"
|
||||
|
||||
type Task = {
|
||||
status: string
|
||||
dueAt: string
|
||||
createdBy: {
|
||||
workspaceMemberId: string
|
||||
}
|
||||
}
|
||||
|
||||
export const summariseTaskCreation = async (date: string) => {
|
||||
const { tasks }: { tasks: Task[] } = await request(
|
||||
`tasks?filter=createdAt[gte]:${date}`,
|
||||
)
|
||||
if (tasks.length === 0) {
|
||||
return '- No Tasks were added'
|
||||
}
|
||||
|
||||
const workspaceMemberIdsDueDateCounter: Record<string, number> = {}
|
||||
let workspaceMembers = []
|
||||
const completedTasks: Task[] = []
|
||||
|
||||
const pendingTasksWithPastDueDates = tasks.reduce((collection: Task[], task) => {
|
||||
if (task.status === 'DONE') {
|
||||
completedTasks.push(task)
|
||||
return collection
|
||||
}
|
||||
|
||||
if (new Date().toISOString() > 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}`)
|
||||
if (data.workspaceMember) {
|
||||
workspaceMembers.push({
|
||||
...data.workspaceMember,
|
||||
slackCount: count
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let slackerMessage = workspaceMembers.length > 0 ? workspaceMembers.reduce((text, member, index) => {
|
||||
if (index === 0 && workspaceMembers.length === 1) {
|
||||
return `${member.name.firstName} slacked the most with ${member.slackCount} Tasks overdue`
|
||||
}
|
||||
|
||||
if (index !== workspaceMembers.length - 1) {
|
||||
return text.concat(`, ${member.name.firstName}`)
|
||||
}
|
||||
|
||||
if (index === workspaceMembers.length - 1) {
|
||||
return text.concat(`, and ${member.name.firstName} slacked the most with ${member.slackCount} Tasks overdue`)
|
||||
}
|
||||
}, '') : '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`
|
||||
|
||||
return `- ${tasks.length} Tasks were created
|
||||
- ${taskCompletionMessage}
|
||||
- ${slackerMessage}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @description This is generated by AI
|
||||
*/
|
||||
const findMaxIncompleteKeys = (workspaceMemberIdsDueDateCounter: Record<string,number>) => {
|
||||
// Get the maximum incomplete count
|
||||
const max = Math.max(...Object.values(workspaceMemberIdsDueDateCounter));
|
||||
|
||||
// Filter keys that match the maximum value
|
||||
return Object.entries(workspaceMemberIdsDueDateCounter)
|
||||
.filter(([_, count]) => count === max)
|
||||
.map(([userId, count]) => ({ userId, count }))
|
||||
};
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
export const request = async (route: string, authToken?: string) => {
|
||||
const response = await fetch(
|
||||
`https://api.twenty.com/rest/${route}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.TWENTY_API_KEY}`
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const json = await response.json()
|
||||
return json.data
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# This file is generated by running "yarn install" inside your project.
|
||||
# Manual changes might be lost - proceed with caution!
|
||||
|
||||
__metadata:
|
||||
version: 8
|
||||
cacheKey: 10c0
|
||||
|
||||
"@types/node@npm:^24.7.2":
|
||||
version: 24.9.2
|
||||
resolution: "@types/node@npm:24.9.2"
|
||||
dependencies:
|
||||
undici-types: "npm:~7.16.0"
|
||||
checksum: 10c0/7905d43f65cee72ef475fe76316e10bbf6ac5d08a7f0f6c38f2b6285d7ca3009e8fcafc8f8a1d2bf3f55889c9c278dbb203a9081fd0cf2d6d62161703924c6fa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"Activity Summary@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "Activity Summary@workspace:."
|
||||
dependencies:
|
||||
"@types/node": "npm:^24.7.2"
|
||||
twenty-sdk: "npm:0.0.3"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"twenty-sdk@npm:0.0.3":
|
||||
version: 0.0.3
|
||||
resolution: "twenty-sdk@npm:0.0.3"
|
||||
checksum: 10c0/0a3c85c27edb22fb50f7eb0da4f9770e85729fce05e9e0118ad0cdfc36e42425c93340a6cd1c276daf30aeeaa612db0cd905831c0a8287a31bff3da5be9b0562
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"undici-types@npm:~7.16.0":
|
||||
version: 7.16.0
|
||||
resolution: "undici-types@npm:7.16.0"
|
||||
checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
Reference in New Issue
Block a user