Skip to content

Instantly share code, notes, and snippets.

@collymore
Created February 21, 2025 10:10
Show Gist options
  • Save collymore/da6a6d2a4beaade5dda04c9b53c270c0 to your computer and use it in GitHub Desktop.
Save collymore/da6a6d2a4beaade5dda04c9b53c270c0 to your computer and use it in GitHub Desktop.
Toggle and Slack Client updates Napkin JS
export default async (request, response) => {
// Map Toggl client IDs to Slack channel IDs (private or public)
const clientToSlackChannel = {
61586288: 'C04D3USRW1Z', // Client 1 ID Toggl : Slack Channel
53983123: 'C01PCCUFXN0', // Client 2
53983123: 'C01PCCUFXN0', // Client 3
};
// 1) Prepare date range for the current month
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const startDate = `${year}-${month}-01`;
// End date: first day of next month
const endDateObj = new Date(year, now.getMonth() + 1, 1);
const endDate = endDateObj.toISOString().split('T')[0];
try {
// Process all clients in parallel (or you could do .forEach and await sequentially)
const results = await Promise.all(
Object.entries(clientToSlackChannel).map(async ([clientId, slackChannel]) => {
// Build Toggl URL for each client
const togglUrl =
'https://api.track.toggl.com/reports/api/v2/summary'
+ `?workspace_id=${process.env.TOGGL_WORKSPACE_ID}`
+ `&client_ids=${clientId}`
+ `&since=${startDate}`
+ `&until=${endDate}`;
// Prepare authorization
const togglAuth = Buffer.from(
process.env.TOGGL_API_TOKEN + ':api_token'
).toString('base64');
// Fetch Toggl data
const togglResponse = await fetch(togglUrl, {
method: 'GET',
headers: {
Authorization: `Basic ${togglAuth}`,
'Content-Type': 'application/json',
},
});
if (!togglResponse.ok) {
const errorText = await togglResponse.text();
// Return an error-like result for this client
return {
clientId,
slackChannel,
error: `Toggl API error: ${errorText}`,
};
}
const togglData = await togglResponse.json();
// If no data returned, post a Slack message for zero usage
if (!togglData.data || togglData.data.length === 0) {
await postToSlack(
slackChannel,
`No time logged this month for client ID: ${clientId}`
);
return {
clientId,
slackChannel,
clientName: `Client ID ${clientId}`,
totalHours: 0,
message: 'No usage data found',
};
}
// Build a report for each project
let totalSecondsAllProjects = 0;
const projectLines = [];
togglData.data.forEach(item => {
const projectName = item.title?.project || 'Unnamed Project';
const totalSeconds = item.time || item.total_grand || 0;
totalSecondsAllProjects += totalSeconds;
const hoursThisProject = (totalSeconds / 3600000).toFixed(2);
projectLines.push(`• *${projectName}*: ${hoursThisProject}h`);
});
// Summaries (client name might be togglData.data[0].title.client)
const clientName =
togglData.data[0].title?.client || `Client ID ${clientId}`;
const totalHours = (totalSecondsAllProjects / 3600000).toFixed(2);
// Slack message
const messageText = [
`*${clientName} usage this month:*`,
...projectLines,
`*Total:* ${totalHours}h`
].join('\n');
// Post to Slack
await postToSlack(slackChannel, messageText);
return {
clientId,
slackChannel,
clientName,
totalHours,
message: `Successfully sent usage data for ${clientName} to channel ${slackChannel}`,
};
})
);
// 3) Return the combined results for all clients
response.json({
message: 'All clients processed successfully',
results,
});
} catch (err) {
response.status(500).send(`Unexpected error: ${err.message}`);
}
};
/**
* Helper function: Post a message to Slack
*/
async function postToSlack(channel, text) {
const slackResult = await fetch('https://slack.com/api/chat.postMessage', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SLACK_BOT_TOKEN}`,
'Content-Type': 'application/json; charset=utf-8',
},
body: JSON.stringify({ channel, text }),
});
const slackData = await slackResult.json();
if (!slackData.ok) {
throw new Error(`Slack API error: ${slackData.error || 'unknown error'}`);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment