Skip to main content
Glama
goals-enhanced.ts12.8 kB
import { ClickUpClient } from './index.js'; import axios from 'axios'; // ======================================== // GOALS TYPE DEFINITIONS // ======================================== export interface GoalMember { id: number; username: string; email: string; color: string; initials: string; profilePicture: string; } export interface CreateGoalParams { name: string; due_date: number; // Unix timestamp description?: string; multiple_owners: boolean; owners: number[]; color?: string; } export interface UpdateGoalParams { name?: string; due_date?: number; description?: string; rem_owners?: number[]; add_owners?: number[]; color?: string; } export interface GoalTarget { id: string; goal_id: string; name: string; creator: number; type: 'number' | 'currency' | 'boolean' | 'task' | 'list'; date_created: string; start_value: number; target_value: number; current_value: number; unit: string | null; task_statuses: string[] | null; list_ids: string[] | null; completed: boolean; percent_completed: number; } export interface Goal { id: string; name: string; team_id: string; date_created: string; start_date: string | null; due_date: string; description: string; private: boolean; archived: boolean; creator: number; color: string; pretty_id: string; multiple_owners: boolean; folder_id: string | null; members: GoalMember[]; owners: GoalMember[]; key_results: GoalTarget[]; percent_completed: number; pretty_url: string; } export interface CreateGoalTargetParams { name: string; type: 'number' | 'currency' | 'boolean' | 'task' | 'list'; target_value: number; start_value?: number; unit?: string; task_statuses?: string[]; list_ids?: string[]; } export interface UpdateGoalTargetParams { name?: string; target_value?: number; unit?: string; task_statuses?: string[]; list_ids?: string[]; } export interface GoalSummary { total_goals: number; completed_goals: number; in_progress_goals: number; overdue_goals: number; average_progress: number; goals_by_status: Record<string, number>; upcoming_deadlines: Array<{ goal_id: string; name: string; due_date: string; days_remaining: number; }>; } // ======================================== // ENHANCED GOALS CLIENT // ======================================== export class EnhancedGoalsClient { private client: ClickUpClient; constructor(client: ClickUpClient) { this.client = client; } private getAxiosInstance() { return this.client.getAxiosInstance(); } // ======================================== // GOAL MANAGEMENT // ======================================== /** * Get goals for a team */ async getGoals(teamId: string, includeCompleted: boolean = false): Promise<Goal[]> { try { const queryParams = new URLSearchParams(); if (includeCompleted) queryParams.append('include_completed', 'true'); const endpoint = `/team/${teamId}/goal?${queryParams.toString()}`; const response = await this.getAxiosInstance().get(endpoint); return response.data.goals || []; } catch (error) { console.error('Error getting goals:', error); throw this.handleError(error, `Failed to get goals for team ${teamId}`); } } /** * Create a new goal */ async createGoal(teamId: string, params: CreateGoalParams): Promise<Goal> { try { const endpoint = `/team/${teamId}/goal`; const response = await this.getAxiosInstance().post(endpoint, params); return response.data.goal; } catch (error) { console.error('Error creating goal:', error); throw this.handleError(error, `Failed to create goal for team ${teamId}`); } } /** * Update an existing goal */ async updateGoal(goalId: string, params: UpdateGoalParams): Promise<Goal> { try { const endpoint = `/goal/${goalId}`; const response = await this.getAxiosInstance().put(endpoint, params); return response.data.goal; } catch (error) { console.error('Error updating goal:', error); throw this.handleError(error, `Failed to update goal ${goalId}`); } } /** * Delete a goal */ async deleteGoal(goalId: string): Promise<void> { try { const endpoint = `/goal/${goalId}`; await this.getAxiosInstance().delete(endpoint); } catch (error) { console.error('Error deleting goal:', error); throw this.handleError(error, `Failed to delete goal ${goalId}`); } } /** * Get a specific goal by ID */ async getGoal(goalId: string): Promise<Goal> { try { const endpoint = `/goal/${goalId}`; const response = await this.getAxiosInstance().get(endpoint); return response.data.goal; } catch (error) { console.error('Error getting goal:', error); throw this.handleError(error, `Failed to get goal ${goalId}`); } } // ======================================== // GOAL TARGET MANAGEMENT // ======================================== /** * Create a goal target (key result) */ async createGoalTarget(goalId: string, params: CreateGoalTargetParams): Promise<GoalTarget> { try { const endpoint = `/goal/${goalId}/target`; const response = await this.getAxiosInstance().post(endpoint, params); return response.data.target; } catch (error) { console.error('Error creating goal target:', error); throw this.handleError(error, `Failed to create target for goal ${goalId}`); } } /** * Update a goal target */ async updateGoalTarget(goalId: string, targetId: string, params: UpdateGoalTargetParams): Promise<GoalTarget> { try { const endpoint = `/goal/${goalId}/target/${targetId}`; const response = await this.getAxiosInstance().put(endpoint, params); return response.data.target; } catch (error) { console.error('Error updating goal target:', error); throw this.handleError(error, `Failed to update target ${targetId} for goal ${goalId}`); } } /** * Delete a goal target */ async deleteGoalTarget(goalId: string, targetId: string): Promise<void> { try { const endpoint = `/goal/${goalId}/target/${targetId}`; await this.getAxiosInstance().delete(endpoint); } catch (error) { console.error('Error deleting goal target:', error); throw this.handleError(error, `Failed to delete target ${targetId} for goal ${goalId}`); } } // ======================================== // GOAL ANALYTICS & REPORTING // ======================================== /** * Get goal summary and analytics for a team */ async getGoalSummary(teamId: string): Promise<GoalSummary> { try { const goals = await this.getGoals(teamId, true); const totalGoals = goals.length; let completedGoals = 0; let inProgressGoals = 0; let overdueGoals = 0; let totalProgress = 0; const goalsByStatus: Record<string, number> = {}; const upcomingDeadlines: Array<{ goal_id: string; name: string; due_date: string; days_remaining: number; }> = []; const now = Date.now(); for (const goal of goals) { const dueDate = new Date(goal.due_date).getTime(); const progress = goal.percent_completed; totalProgress += progress; // Categorize goals if (progress >= 100) { completedGoals++; } else if (now > dueDate) { overdueGoals++; } else { inProgressGoals++; } // Get goal status const status = this.getGoalStatus(progress, goal.due_date); goalsByStatus[status] = (goalsByStatus[status] || 0) + 1; // Check for upcoming deadlines (within 7 days) const daysRemaining = Math.ceil((dueDate - now) / (1000 * 60 * 60 * 24)); if (daysRemaining > 0 && daysRemaining <= 7 && progress < 100) { upcomingDeadlines.push({ goal_id: goal.id, name: goal.name, due_date: goal.due_date, days_remaining: daysRemaining }); } } // Sort upcoming deadlines by days remaining upcomingDeadlines.sort((a, b) => a.days_remaining - b.days_remaining); return { total_goals: totalGoals, completed_goals: completedGoals, in_progress_goals: inProgressGoals, overdue_goals: overdueGoals, average_progress: totalGoals > 0 ? Math.round(totalProgress / totalGoals) : 0, goals_by_status: goalsByStatus, upcoming_deadlines: upcomingDeadlines }; } catch (error) { console.error('Error getting goal summary:', error); throw this.handleError(error, `Failed to get goal summary for team ${teamId}`); } } // ======================================== // UTILITY METHODS // ======================================== /** * Calculate progress percentage for a target */ calculateTargetProgress(startValue: number, currentValue: number, targetValue: number): number { if (targetValue === startValue) return 100; // Avoid division by zero const progress = ((currentValue - startValue) / (targetValue - startValue)) * 100; return Math.min(Math.max(progress, 0), 100); // Clamp between 0 and 100 } /** * Check if a target is completed */ isTargetCompleted(currentValue: number, targetValue: number, type: string): boolean { switch (type) { case 'boolean': return currentValue >= 1; case 'number': case 'currency': case 'task': case 'list': return currentValue >= targetValue; default: return false; } } /** * Format currency value with unit */ formatCurrencyValue(value: number, unit: string = 'USD'): string { const formatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: unit.toUpperCase(), minimumFractionDigits: 0, maximumFractionDigits: 2 }); try { return formatter.format(value); } catch (error) { // Fallback for invalid currency codes return `${unit} ${value.toLocaleString()}`; } } /** * Format number value with unit */ formatNumberValue(value: number, unit?: string): string { const formattedNumber = value.toLocaleString(); return unit ? `${formattedNumber} ${unit}` : formattedNumber; } /** * Get goal status based on progress and due date */ getGoalStatus(percentCompleted: number, dueDate: string): 'completed' | 'on_track' | 'at_risk' | 'overdue' { const now = Date.now(); const due = new Date(dueDate).getTime(); if (percentCompleted >= 100) return 'completed'; if (now > due) return 'overdue'; // Calculate if on track (simple heuristic: progress should match time elapsed) const timeElapsed = now; const totalTime = due; const expectedProgress = (timeElapsed / totalTime) * 100; if (percentCompleted >= expectedProgress * 0.8) return 'on_track'; return 'at_risk'; } /** * Calculate days until due date */ getDaysUntilDue(dueDate: string): number { const now = Date.now(); const due = new Date(dueDate).getTime(); return Math.ceil((due - now) / (1000 * 60 * 60 * 24)); } /** * Validate goal date (must be in the future) */ validateGoalDate(dueDate: number): boolean { const now = Date.now(); return dueDate > now; } // ======================================== // ERROR HANDLING // ======================================== private handleError(error: any, context: string): Error { if (axios.isAxiosError(error)) { const status = error.response?.status; const message = error.response?.data?.message || error.message; switch (status) { case 400: return new Error(`${context}: Invalid request - ${message}`); case 401: return new Error(`${context}: Authentication failed - check API token`); case 403: return new Error(`${context}: Permission denied - insufficient access rights`); case 404: return new Error(`${context}: Resource not found - ${message}`); case 429: return new Error(`${context}: Rate limit exceeded - please retry later`); case 500: return new Error(`${context}: Server error - please try again`); default: return new Error(`${context}: ${message}`); } } return new Error(`${context}: ${error.message || 'Unknown error'}`); } } export const createEnhancedGoalsClient = (client: ClickUpClient): EnhancedGoalsClient => { return new EnhancedGoalsClient(client); };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Chykalophia/ClickUp-MCP-Server---Enhanced'

If you have feedback or need assistance with the MCP directory API, please join our Discord server