@aot-tech/clockify-mcp-server
Version:
MCP Server for Clockify time tracking integration with AI tools
789 lines (788 loc) • 34.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.multiUserTimeEntriestool = exports.teamProductivityReportTool = exports.bulkUserTimeAnalyticsTool = void 0;
const zod_1 = require("zod");
const entries_1 = require("../clockify-sdk/entries");
const users_1 = require("../clockify-sdk/users");
const projects_1 = require("../clockify-sdk/projects");
const reports_1 = require("../clockify-sdk/reports");
const isLocalDevelopment = process.env.NODE_ENV === 'development' || process.env.DEBUG_MODE === 'true';
function debugLog(message, data) {
if (!isLocalDevelopment) {
return;
}
const timestamp = new Date().toISOString();
if (data) {
console.log(`[CLOCKIFY-MCP-DEBUG] [${timestamp}] ${message}`, data);
}
else {
console.log(`[CLOCKIFY-MCP-DEBUG] [${timestamp}] ${message}`);
}
}
const bulkCache = new Map();
const CACHE_DURATION = 5 * 60 * 1000;
const invalidateUserCache = (userId) => {
for (const [key, value] of bulkCache.entries()) {
if (key.includes(userId)) {
bulkCache.delete(key);
}
}
};
const CACHE_VERSION = "v1";
function getCacheKey(prefix, params) {
const normalizedParams = {};
normalizedParams.workspaceId = params.workspaceId;
normalizedParams.userIds = Array.isArray(params.userIds)
? [...params.userIds].sort()
: params.userIds;
normalizedParams.startDate =
params.startDate instanceof Date
? params.startDate.toISOString().split("T")[0]
: params.startDate;
normalizedParams.endDate =
params.endDate instanceof Date
? params.endDate.toISOString().split("T")[0]
: params.endDate;
normalizedParams.includeProjects = Boolean(params.includeProjects);
normalizedParams.includeCorrelation = Boolean(params.includeCorrelation);
normalizedParams.aggregateBy = params.aggregateBy || "user";
if (params.projectIds && Array.isArray(params.projectIds)) {
normalizedParams.projectIds = [...params.projectIds].sort();
}
if (params.includeDetailed !== undefined) {
normalizedParams.includeDetailed = Boolean(params.includeDetailed);
}
const sortedParams = Object.keys(normalizedParams)
.sort()
.reduce((result, key) => {
result[key] = normalizedParams[key];
return result;
}, {});
const cacheKey = `${CACHE_VERSION}_${prefix}_${JSON.stringify(sortedParams)}`;
debugLog(`Generated cache key for ${prefix}:`, {
originalParams: params,
normalizedParams: sortedParams,
cacheKey: cacheKey.substring(0, 200) + "...",
});
return cacheKey;
}
function isValidCache(key) {
const cached = bulkCache.get(key);
const isValid = cached !== undefined && Date.now() - cached.timestamp < CACHE_DURATION;
const age = cached ? Date.now() - cached.timestamp : 0;
debugLog(`Cache validation for key: ${key.substring(0, 100)}...`, {
exists: cached !== undefined,
age: age,
maxAge: CACHE_DURATION,
isValid: isValid,
});
return isValid;
}
function setCache(key, data) {
const timestamp = Date.now();
bulkCache.set(key, { data, timestamp });
debugLog(`Cache set for key: ${key.substring(0, 100)}...`, {
timestamp: timestamp,
cacheSize: bulkCache.size,
});
}
function getCache(key) {
const cached = bulkCache.get(key);
debugLog(`Cache get for key: ${key.substring(0, 100)}...`, {
found: cached !== undefined,
age: cached ? Date.now() - cached.timestamp : 0,
});
return cached?.data;
}
exports.bulkUserTimeAnalyticsTool = {
name: "clockify_bulk_user_time_analytics",
description: "OPTIMIZED: Get comprehensive time analytics for multiple users in one call. Ideal for team productivity analysis and performance ranking.",
parameters: {
workspaceId: zod_1.z.string().describe("Workspace ID"),
userIds: zod_1.z.array(zod_1.z.string()).describe("Array of user IDs to analyze"),
startDate: zod_1.z.coerce.date().describe("Start date for analysis"),
endDate: zod_1.z.coerce.date().describe("End date for analysis"),
includeProjects: zod_1.z
.boolean()
.optional()
.default(true)
.describe("Include project breakdowns"),
includeCorrelation: zod_1.z
.boolean()
.optional()
.default(true)
.describe("Include correlation data for cross-platform analysis"),
aggregateBy: zod_1.z
.enum(["user", "project", "day"])
.optional()
.default("user")
.describe("How to aggregate the results"),
},
handler: async (params) => {
try {
const cacheKey = getCacheKey("bulk_user_time_analytics", params);
debugLog("Bulk User Time Analytics - Parameters:", {
workspaceId: params.workspaceId,
userIds: params.userIds,
startDate: params.startDate.toISOString(),
endDate: params.endDate.toISOString(),
includeProjects: params.includeProjects,
includeCorrelation: params.includeCorrelation,
aggregateBy: params.aggregateBy,
});
debugLog("Cache key generated:", cacheKey.substring(0, 200) + "...");
if (isValidCache(cacheKey)) {
debugLog("Using cached results for bulk user time analytics");
return {
content: [
{
type: "text",
text: JSON.stringify(getCache(cacheKey), null, 2),
},
],
};
}
debugLog("Cache miss - executing fresh API calls for bulk user time analytics");
debugLog(`Processing ${params.userIds.length} users for time analytics`);
const userPromises = params.userIds.map(async (userId) => {
debugLog(`Fetching time entries for user: ${userId}`);
const timeEntriesResponse = await entries_1.entriesService.find({
workspaceId: params.workspaceId,
userId: userId,
start: params.startDate,
end: params.endDate,
});
const timeEntries = timeEntriesResponse.entries.data || [];
debugLog(`User ${userId}: Found ${timeEntries.length} time entries`);
const totalDuration = timeEntries.reduce((sum, entry) => {
const duration = calculateDuration(entry.timeInterval.start, entry.timeInterval.end);
return sum + duration;
}, 0);
const billableTime = timeEntries
.filter((entry) => entry.billable)
.reduce((sum, entry) => {
const duration = calculateDuration(entry.timeInterval.start, entry.timeInterval.end);
return sum + duration;
}, 0);
const projectBreakdown = params.includeProjects
? groupByProject(timeEntries)
: null;
const dailyBreakdown = groupByDate(timeEntries);
const userInfo = await getUserInfo(userId, params.workspaceId);
const metrics = {
totalHours: totalDuration / 3600,
billableHours: billableTime / 3600,
nonBillableHours: (totalDuration - billableTime) / 3600,
billablePercentage: totalDuration > 0 ? (billableTime / totalDuration) * 100 : 0,
totalSessions: timeEntries.length,
avgSessionDuration: timeEntries.length > 0
? totalDuration / timeEntries.length / 3600
: 0,
workingDays: Object.keys(dailyBreakdown).length,
avgHoursPerDay: Object.keys(dailyBreakdown).length > 0
? totalDuration / Object.keys(dailyBreakdown).length / 3600
: 0,
};
debugLog(`User ${userId} metrics:`, {
totalHours: metrics.totalHours,
billableHours: metrics.billableHours,
billablePercentage: metrics.billablePercentage,
totalSessions: metrics.totalSessions,
});
return {
userId,
userInfo,
metrics,
projectBreakdown,
dailyBreakdown,
correlationData: params.includeCorrelation
? {
clockifyUserId: userId,
clockifyUserName: userInfo.name,
clockifyUserEmail: userInfo.email,
jiraCorrelationKey: (userInfo.email || userInfo.name)
.toLowerCase()
.replace(/\s+/g, "."),
projectIds: [
...new Set(timeEntries
.map((entry) => entry.projectId)
.filter(Boolean)),
],
timeframe: {
startDate: params.startDate.toISOString(),
endDate: params.endDate.toISOString(),
totalDays: Math.ceil((params.endDate.getTime() - params.startDate.getTime()) /
(1000 * 60 * 60 * 24)),
},
}
: null,
};
});
const userAnalytics = await Promise.all(userPromises);
debugLog(`Completed processing ${userAnalytics.length} users for time analytics`);
const teamSummary = {
totalUsers: userAnalytics.length,
totalHours: userAnalytics.reduce((sum, user) => sum + user.metrics.totalHours, 0),
totalBillableHours: userAnalytics.reduce((sum, user) => sum + user.metrics.billableHours, 0),
avgHoursPerUser: userAnalytics.reduce((sum, user) => sum + user.metrics.totalHours, 0) / userAnalytics.length,
avgBillablePercentage: userAnalytics.reduce((sum, user) => sum + user.metrics.billablePercentage, 0) / userAnalytics.length,
totalSessions: userAnalytics.reduce((sum, user) => sum + user.metrics.totalSessions, 0),
topPerformer: userAnalytics.reduce((best, current) => current.metrics.totalHours > best.metrics.totalHours ? current : best),
};
const rankedUsers = userAnalytics
.sort((a, b) => b.metrics.totalHours - a.metrics.totalHours)
.map((user, index) => ({ ...user, rank: index + 1 }));
const insights = generateTeamInsights(userAnalytics, teamSummary);
const result = {
summary: teamSummary,
users: rankedUsers,
insights,
aggregation: params.aggregateBy,
timeframe: {
startDate: params.startDate.toISOString(),
endDate: params.endDate.toISOString(),
},
metadata: {
workspaceId: params.workspaceId,
usersAnalyzed: params.userIds.length,
includeProjects: params.includeProjects,
includeCorrelation: params.includeCorrelation,
generatedAt: new Date().toISOString(),
},
};
setCache(cacheKey, result);
debugLog("Result cached successfully for bulk user time analytics");
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
catch (error) {
return {
content: [
{
type: "text",
text: `Error in bulk user time analytics: ${error instanceof Error ? error.message : "Unknown error"}`,
},
],
};
}
},
};
exports.teamProductivityReportTool = {
name: "clockify_team_productivity_report",
description: "OPTIMIZED: Generate comprehensive team productivity report with cross-platform correlation data and performance metrics.",
parameters: {
workspaceId: zod_1.z.string().describe("Workspace ID"),
projectIds: zod_1.z
.array(zod_1.z.string())
.optional()
.describe("Optional array of project IDs to filter by"),
userIds: zod_1.z
.array(zod_1.z.string())
.optional()
.describe("Optional array of user IDs to filter by (if not provided, gets all users)"),
startDate: zod_1.z.coerce.date().describe("Start date for the report"),
endDate: zod_1.z.coerce.date().describe("End date for the report"),
includeDetailed: zod_1.z
.boolean()
.default(true)
.describe("Include detailed breakdowns"),
includeCorrelation: zod_1.z
.boolean()
.default(true)
.describe("Include correlation data for cross-platform analysis"),
},
handler: async (params) => {
try {
const cacheKey = getCacheKey("team_productivity_report", params);
debugLog("Team Productivity Report - Parameters:", {
workspaceId: params.workspaceId,
projectIds: params.projectIds,
userIds: params.userIds,
startDate: params.startDate.toISOString(),
endDate: params.endDate.toISOString(),
includeDetailed: params.includeDetailed,
includeCorrelation: params.includeCorrelation,
});
debugLog("Cache key generated:", cacheKey.substring(0, 200) + "...");
if (isValidCache(cacheKey)) {
debugLog("Using cached results for team productivity report");
return {
content: [
{
type: "text",
text: JSON.stringify(getCache(cacheKey), null, 2),
},
],
};
}
debugLog("Cache miss - executing fresh API calls for team productivity report");
debugLog("Generating summary report from Clockify API");
const summaryResponse = await reports_1.reportsService.getSummaryReport({
workspaceId: params.workspaceId,
dateRangeStart: params.startDate,
dateRangeEnd: params.endDate,
projects: params.projectIds,
groups: ["USER", "TIMEENTRY"]
});
const summaryData = summaryResponse.data || summaryResponse;
debugLog("Summary report data received from Clockify API");
const result = summaryData;
setCache(cacheKey, result);
debugLog("Result cached successfully for team productivity report");
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
catch (error) {
return {
content: [
{
type: "text",
text: `Error in team productivity report: ${error instanceof Error ? error.message : "Unknown error"}`,
},
],
};
}
},
};
exports.multiUserTimeEntriestool = {
name: "clockify_multi_user_time_entries",
description: "OPTIMIZED: Get time entries for multiple users simultaneously with batch processing and aggregation options.",
parameters: {
workspaceId: zod_1.z.string().describe("Workspace ID"),
userIds: zod_1.z.array(zod_1.z.string()).describe("Array of user IDs"),
startDate: zod_1.z.coerce.date().describe("Start date for time entries"),
endDate: zod_1.z.coerce.date().describe("End date for time entries"),
aggregateBy: zod_1.z
.enum(["user", "project", "day"])
.default("user")
.describe("How to aggregate results"),
includeProjects: zod_1.z
.boolean()
.default(true)
.describe("Include project information"),
batchSize: zod_1.z
.number()
.default(5)
.describe("Number of users to process in parallel (default: 5)"),
},
handler: async (params) => {
try {
const cacheKey = getCacheKey("multi_user_time_entries", params);
if (isValidCache(cacheKey)) {
return {
content: [
{
type: "text",
text: JSON.stringify(getCache(cacheKey), null, 2),
},
],
};
}
const batchSize = params.batchSize;
const userBatches = [];
for (let i = 0; i < params.userIds.length; i += batchSize) {
userBatches.push(params.userIds.slice(i, i + batchSize));
}
const allUserData = [];
for (const batch of userBatches) {
const batchPromises = batch.map(async (userId) => {
const timeEntriesResponse = await entries_1.entriesService.find({
workspaceId: params.workspaceId,
userId: userId,
start: params.startDate,
end: params.endDate,
});
const timeEntries = timeEntriesResponse.entries.data || [];
const userInfo = await getUserInfo(userId, params.workspaceId);
return {
userId,
userInfo,
timeEntries,
summary: {
totalEntries: timeEntries.length,
totalHours: timeEntries.reduce((sum, entry) => {
const duration = calculateDuration(entry.timeInterval.start, entry.timeInterval.end);
return sum + duration;
}, 0) / 3600,
billableHours: timeEntries
.filter((entry) => entry.billable)
.reduce((sum, entry) => {
const duration = calculateDuration(entry.timeInterval.start, entry.timeInterval.end);
return sum + duration;
}, 0) / 3600,
projectCount: new Set(timeEntries.map((entry) => entry.projectId).filter(Boolean)).size,
dateRange: {
startDate: params.startDate.toISOString(),
endDate: params.endDate.toISOString(),
},
},
};
});
const batchResults = await Promise.all(batchPromises);
allUserData.push(...batchResults);
}
let aggregatedData = {};
switch (params.aggregateBy) {
case "user":
aggregatedData = aggregateByUser(allUserData);
break;
case "project":
aggregatedData = await aggregateByProject(allUserData, params.workspaceId, params.includeProjects);
break;
case "day":
aggregatedData = aggregateByDay(allUserData);
break;
}
const overallSummary = {
totalUsers: allUserData.length,
totalEntries: allUserData.reduce((sum, user) => sum + user.summary.totalEntries, 0),
totalHours: allUserData.reduce((sum, user) => sum + user.summary.totalHours, 0),
totalBillableHours: allUserData.reduce((sum, user) => sum + user.summary.billableHours, 0),
avgHoursPerUser: allUserData.reduce((sum, user) => sum + user.summary.totalHours, 0) /
allUserData.length,
totalProjectsInvolved: new Set(allUserData.flatMap((user) => user.timeEntries
.map((entry) => entry.projectId)
.filter(Boolean))).size,
dateRange: {
startDate: params.startDate.toISOString(),
endDate: params.endDate.toISOString(),
},
};
const result = {
overallSummary,
aggregatedData,
userDetails: allUserData,
aggregationType: params.aggregateBy,
metadata: {
workspaceId: params.workspaceId,
batchSize: params.batchSize,
processedBatches: userBatches.length,
includeProjects: params.includeProjects,
generatedAt: new Date().toISOString(),
},
};
setCache(cacheKey, result);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
catch (error) {
return {
content: [
{
type: "text",
text: `Error in multi-user time entries: ${error instanceof Error ? error.message : "Unknown error"}`,
},
],
};
}
},
};
function calculateDuration(start, end) {
if (!end)
return 0;
return (new Date(end).getTime() - new Date(start).getTime()) / 1000;
}
function calculateConsistencyScore(entries) {
if (entries.length === 0)
return 0;
const dailyTotals = groupByDate(entries);
const dailyHours = Object.values(dailyTotals).map((total) => total / 3600);
if (dailyHours.length <= 1)
return 100;
const mean = dailyHours.reduce((sum, hours) => sum + hours, 0) / dailyHours.length;
const variance = dailyHours.reduce((sum, hours) => sum + Math.pow(hours - mean, 2), 0) /
dailyHours.length;
const standardDeviation = Math.sqrt(variance);
return Math.max(0, 100 - standardDeviation * 20);
}
function calculateEfficiency(entries) {
if (entries.length === 0)
return 0;
const totalDuration = entries.reduce((sum, entry) => {
return (sum + calculateDuration(entry.timeInterval.start, entry.timeInterval.end));
}, 0);
const billableDuration = entries
.filter((entry) => entry.billable)
.reduce((sum, entry) => {
return (sum +
calculateDuration(entry.timeInterval.start, entry.timeInterval.end));
}, 0);
return totalDuration > 0 ? (billableDuration / totalDuration) * 100 : 0;
}
function calculateConsistency(dailyBreakdown) {
const days = Object.keys(dailyBreakdown);
if (days.length <= 1)
return 100;
const hours = Object.values(dailyBreakdown).map((seconds) => seconds / 3600);
const mean = hours.reduce((sum, h) => sum + h, 0) / hours.length;
const variance = hours.reduce((sum, h) => sum + Math.pow(h - mean, 2), 0) / hours.length;
const standardDeviation = Math.sqrt(variance);
return Math.max(0, 100 - standardDeviation * 15);
}
function calculateTimeDistribution(entries, totalTime) {
const entryTime = entries.reduce((sum, entry) => {
return (sum + calculateDuration(entry.timeInterval.start, entry.timeInterval.end));
}, 0);
return totalTime > 0 ? (entryTime / totalTime) * 100 : 0;
}
function groupByProject(entries) {
const grouped = {};
entries.forEach((entry) => {
const projectId = entry.projectId || "no-project";
if (!grouped[projectId]) {
grouped[projectId] = {
projectId,
projectName: entry.project?.name || "No Project",
totalTime: 0,
billableTime: 0,
entries: [],
};
}
const duration = calculateDuration(entry.timeInterval.start, entry.timeInterval.end);
grouped[projectId].totalTime += duration;
if (entry.billable) {
grouped[projectId].billableTime += duration;
}
grouped[projectId].entries.push(entry);
});
Object.values(grouped).forEach((project) => {
project.totalHours = project.totalTime / 3600;
project.billableHours = project.billableTime / 3600;
project.billablePercentage =
project.totalTime > 0
? (project.billableTime / project.totalTime) * 100
: 0;
});
return grouped;
}
function groupByDate(entries) {
const grouped = {};
entries.forEach((entry) => {
const date = new Date(entry.timeInterval.start).toISOString().split("T")[0];
const duration = calculateDuration(entry.timeInterval.start, entry.timeInterval.end);
grouped[date] = (grouped[date] || 0) + duration;
});
return grouped;
}
function aggregateByUser(userData) {
const result = {};
userData.forEach((user) => {
result[user.userId] = {
userInfo: user.userInfo,
summary: user.summary,
projectBreakdown: groupByProject(user.timeEntries),
dailyBreakdown: groupByDate(user.timeEntries),
};
});
return result;
}
async function aggregateByProject(userData, workspaceId, includeProjects) {
const projectAggregation = {};
userData.forEach((user) => {
user.timeEntries.forEach((entry) => {
const projectId = entry.projectId || "no-project";
if (!projectAggregation[projectId]) {
projectAggregation[projectId] = {
projectId,
projectName: entry.project?.name || "No Project",
totalTime: 0,
billableTime: 0,
users: new Set(),
entries: [],
};
}
const duration = calculateDuration(entry.timeInterval.start, entry.timeInterval.end);
projectAggregation[projectId].totalTime += duration;
if (entry.billable) {
projectAggregation[projectId].billableTime += duration;
}
projectAggregation[projectId].users.add(user.userId);
projectAggregation[projectId].entries.push(entry);
});
});
const result = {};
for (const [projectId, data] of Object.entries(projectAggregation)) {
result[projectId] = {
projectId: data.projectId,
projectName: data.projectName,
totalHours: data.totalTime / 3600,
billableHours: data.billableTime / 3600,
billablePercentage: data.totalTime > 0 ? (data.billableTime / data.totalTime) * 100 : 0,
userCount: data.users.size,
entryCount: data.entries.length,
users: Array.from(data.users),
};
}
return result;
}
function aggregateByDay(userData) {
const dailyAggregation = {};
userData.forEach((user) => {
user.timeEntries.forEach((entry) => {
const date = new Date(entry.timeInterval.start)
.toISOString()
.split("T")[0];
if (!dailyAggregation[date]) {
dailyAggregation[date] = {
date,
totalTime: 0,
billableTime: 0,
users: new Set(),
entries: [],
};
}
const duration = calculateDuration(entry.timeInterval.start, entry.timeInterval.end);
dailyAggregation[date].totalTime += duration;
if (entry.billable) {
dailyAggregation[date].billableTime += duration;
}
dailyAggregation[date].users.add(user.userId);
dailyAggregation[date].entries.push(entry);
});
});
const result = {};
for (const [date, data] of Object.entries(dailyAggregation)) {
result[date] = {
date: data.date,
totalHours: data.totalTime / 3600,
billableHours: data.billableTime / 3600,
billablePercentage: data.totalTime > 0 ? (data.billableTime / data.totalTime) * 100 : 0,
userCount: data.users.size,
entryCount: data.entries.length,
avgHoursPerUser: data.totalTime / data.users.size / 3600,
};
}
return result;
}
async function getUserInfo(userId, workspaceId) {
try {
const cacheKey = `user_info_${userId}`;
if (isValidCache(cacheKey)) {
return getCache(cacheKey);
}
const usersResponse = await users_1.usersService.getAllUsers({ workspaceId });
const users = usersResponse.data;
debugLog(`getUserInfo: Retrieved ${users?.length || 0} users for workspace ${workspaceId}`);
const user = users?.find((u) => u.id === userId);
if (user) {
debugLog(`getUserInfo: Found user ${userId}:`, {
id: user.id,
name: user.name,
email: user.email
});
}
else {
debugLog(`getUserInfo: User ${userId} not found in workspace ${workspaceId}`);
}
const userInfo = user ? {
id: user.id,
name: user.name || "Unknown User",
email: user.email || null
} : { id: userId, name: "Unknown User", email: null };
setCache(cacheKey, userInfo);
return userInfo;
}
catch (error) {
debugLog(`getUserInfo: Error fetching user ${userId}:`, error);
return { id: userId, name: "Unknown User", email: null };
}
}
async function getProjectInfo(projectId, workspaceId) {
try {
const cacheKey = `project_info_${projectId}`;
if (isValidCache(cacheKey)) {
return getCache(cacheKey);
}
const projectsResponse = await projects_1.projectsService.fetchAll(workspaceId, "");
const projects = projectsResponse.data;
debugLog(`getProjectInfo: Retrieved ${projects?.length || 0} projects for workspace ${workspaceId}`);
const project = projects?.find((p) => p.id === projectId);
if (project) {
debugLog(`getProjectInfo: Found project ${projectId}:`, {
id: project.id,
name: project.name
});
}
else {
debugLog(`getProjectInfo: Project ${projectId} not found in workspace ${workspaceId}`);
}
const projectInfo = project ? {
id: project.id,
name: project.name || "Unknown Project"
} : { id: projectId, name: "Unknown Project" };
setCache(cacheKey, projectInfo);
return projectInfo;
}
catch (error) {
debugLog(`getProjectInfo: Error fetching project ${projectId}:`, error);
return { id: projectId, name: "Unknown Project" };
}
}
function generateTeamInsights(userAnalytics, teamSummary) {
const insights = [];
if (teamSummary.topPerformer) {
insights.push(`Top performer: ${teamSummary.topPerformer.userInfo.name} with ${teamSummary.topPerformer.metrics.totalHours.toFixed(1)} hours logged`);
}
if (teamSummary.avgBillablePercentage > 0) {
insights.push(`Team average billable percentage: ${teamSummary.avgBillablePercentage.toFixed(1)}%`);
}
const highConsistency = userAnalytics.filter((user) => user.metrics.consistency > 80);
if (highConsistency.length > 0) {
insights.push(`${highConsistency.length} team members show high consistency (>80%)`);
}
const highProductivity = userAnalytics.filter((user) => user.metrics.billablePercentage > 75);
if (highProductivity.length > 0) {
insights.push(`${highProductivity.length} team members have high productivity scores (>75)`);
}
if (teamSummary.avgSessionDuration > 0) {
insights.push(`Average session duration: ${teamSummary.avgSessionDuration.toFixed(1)} hours`);
}
return insights;
}
function generateTeamProductivityInsights(userAnalytics, projectAnalytics, teamSummary) {
const insights = [];
insights.push(`Team logged ${teamSummary.totalHours.toFixed(1)} hours across ${teamSummary.totalProjects} projects`);
if (teamSummary.topPerformer) {
insights.push(`Most productive: ${teamSummary.topPerformer.userInfo.name} (${teamSummary.topPerformer.metrics.totalHours.toFixed(1)} hours)`);
}
if (teamSummary.mostActiveProject) {
insights.push(`Most active project: ${teamSummary.mostActiveProject.projectInfo.name} (${teamSummary.mostActiveProject.metrics.totalHours.toFixed(1)} hours)`);
}
const billableRate = teamSummary.avgBillablePercentage;
if (billableRate > 80) {
insights.push(`Excellent billable rate: ${billableRate.toFixed(1)}%`);
}
else if (billableRate > 60) {
insights.push(`Good billable rate: ${billableRate.toFixed(1)}%`);
}
else {
insights.push(`Billable rate needs improvement: ${billableRate.toFixed(1)}%`);
}
const highPerformers = userAnalytics.filter((user) => user.metrics.billablePercentage > 75);
if (highPerformers.length > 0) {
insights.push(`${highPerformers.length} high performers with billable efficiency >75%`);
}
const highEfficiency = userAnalytics.filter((user) => user.metrics.billablePercentage > 75);
if (highEfficiency.length > 0) {
insights.push(`${highEfficiency.length} team members have high billable efficiency (>75%)`);
}
return insights;
}