Skip to main content
Glama

Time Tracking MCP

by markwharton
report-formatters.ts4.14 kB
// src/utils/report-formatters.ts /** * Shared formatting utilities for report generation * Following DRY principle - single source of truth for report formatting */ import type { CompanyConfig, DailySummary } from '../types/index.js'; import { SummaryCalculator } from '../services/summary-calculator.js'; import { capitalizeName } from './string-utils.js'; // Note: We lazily create SummaryCalculator instances instead of a singleton // to avoid circular dependency issues with string-utils /** * Sort daily summaries in descending order by date * @param days - Array of daily summaries * @returns New sorted array (does not mutate input) */ export function sortDaysDescending(days: DailySummary[]): DailySummary[] { return [...days].sort((a, b) => b.date.localeCompare(a.date)); } /** * Format total hours with limit and percentage * @param totalHours - Total hours worked * @param config - Company configuration containing limits * @param options - Formatting options * @returns Formatted total hours string */ export function formatTotalHours( totalHours: number, config: CompanyConfig, options?: { showRemaining?: boolean } ): string { let output = `**Total:** ${totalHours.toFixed(1)}h`; const totalLimit = config.commitments.total?.limit; if (totalLimit) { const percent = Math.round((totalHours / totalLimit) * 100); output += ` / ${totalLimit}h (${percent}%)`; if (options?.showRemaining) { const remaining = Math.max(0, totalLimit - totalHours); output += ` (${remaining.toFixed(1)}h remaining)`; } } return output + '\n\n'; } /** * Format commitment breakdown with hours, limits, and status indicators * @param byCommitment - Map of commitment names to hours * @param config - Company configuration containing limits * @returns Formatted commitment breakdown string, or empty string if no commitments */ export function formatCommitmentBreakdown( byCommitment: Record<string, number>, config: CompanyConfig ): string { if (Object.keys(byCommitment).length === 0) { return ''; } const summaryCalculator = new SummaryCalculator(); let output = `**By Commitment:**\n`; for (const [commitment, hours] of Object.entries(byCommitment)) { const limit = config.commitments[commitment]?.limit; const name = capitalizeName(commitment); if (limit) { const stats = summaryCalculator.getCommitmentStats(hours, limit); output += `• ${name}: ${hours.toFixed(1)}h / ${limit}h (${stats.percentage}%)`; // Add status emoji only for approaching or over if (stats.status !== 'within') { output += ` ${stats.emoji}`; } output += '\n'; } else { output += `• ${name}: ${hours.toFixed(1)}h\n`; } } return output + '\n'; } /** * Format project breakdown section * @param byProject - Map of project names to hours * @returns Formatted project breakdown string, or empty string if no projects */ export function formatProjectBreakdown(byProject: Record<string, number>): string { if (Object.keys(byProject).length === 0) { return ''; } let output = `**By Project:**\n`; const sortedProjects = Object.entries(byProject) .sort((a, b) => b[1] - a[1]); // Sort by hours descending for (const [project, hours] of sortedProjects) { output += `• ${project}: ${hours.toFixed(1)}h\n`; } return output + '\n'; } /** * Format tag breakdown section * @param byTag - Map of tag names to hours * @returns Formatted tag breakdown string, or empty string if no tags */ export function formatTagBreakdown(byTag: Record<string, number>): string { if (Object.keys(byTag).length === 0) { return ''; } let output = `**By Tag:**\n`; const sortedTags = Object.entries(byTag) .sort((a, b) => b[1] - a[1]); // Sort by hours descending for (const [tag, hours] of sortedTags) { output += `• #${tag}: ${hours.toFixed(1)}h\n`; } return output + '\n'; }

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/markwharton/time-tracking-mcp'

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