Skip to main content
Glama
logged-time.ts18.9 kB
import { z } from 'zod'; import { createTool } from '../base.js'; import { floatApi } from '../../services/float-api.js'; import { loggedTimeSchema, loggedTimeResponseSchema, type LoggedTime } from '../../types/float.js'; // List logged time entries with comprehensive filtering export const listLoggedTime = createTool( 'list-logged-time', 'List all logged time entries with optional filtering by person, project, date range, and billable status', z.object({ people_id: z.union([z.string(), z.number()]).optional().describe('Filter by person ID'), project_id: z.union([z.string(), z.number()]).optional().describe('Filter by project ID'), task_id: z.string().optional().describe('Filter by task ID'), start_date: z.string().optional().describe('Filter by start date (YYYY-MM-DD) - inclusive'), end_date: z.string().optional().describe('Filter by end date (YYYY-MM-DD) - inclusive'), billable: z .union([z.string(), z.number()]) .optional() .describe('Filter by billable status (1 = billable, 0 = non-billable)'), locked: z .union([z.string(), z.number()]) .optional() .describe('Filter by locked status (1 = locked, 0 = unlocked)'), page: z.number().optional().describe('Page number for pagination'), 'per-page': z.number().optional().describe('Number of items per page (max 200)'), fields: z.string().optional().describe('Comma-separated list of fields to return'), }), async (params) => { const response = await floatApi.getPaginated('/logged-time', params, loggedTimeResponseSchema); return response; } ); // Get specific logged time entry export const getLoggedTime = createTool( 'get-logged-time', 'Get detailed information about a specific logged time entry', z.object({ logged_time_id: z.string().describe('The logged time ID (hexadecimal)'), }), async (params) => { const loggedTime = await floatApi.get( `/logged-time/${params.logged_time_id}`, loggedTimeSchema ); return loggedTime; } ); // Create new logged time entry export const createLoggedTime = createTool( 'create-logged-time', 'Create a new logged time entry for tracking work hours', z.object({ people_id: z.union([z.string(), z.number()]).describe('The person ID who logged the time'), project_id: z.union([z.string(), z.number()]).describe('The project ID for the logged time'), task_id: z.string().optional().describe('The task ID for the logged time'), date: z.string().describe('The date for the logged time (YYYY-MM-DD)'), hours: z.number().describe('The number of hours logged'), billable: z .union([z.string(), z.number()]) .optional() .describe('Whether the time is billable (1 = billable, 0 = non-billable)'), notes: z.string().optional().describe('Optional notes describing the work done'), reference_date: z.string().optional().describe('Reference date for UI suggestions'), }), async (params) => { const loggedTime = await floatApi.post('/logged-time', params, loggedTimeSchema); return loggedTime; } ); // Update existing logged time entry export const updateLoggedTime = createTool( 'update-logged-time', 'Update an existing logged time entry', z.object({ logged_time_id: z.string().describe('The logged time ID (hexadecimal)'), people_id: z .union([z.string(), z.number()]) .optional() .describe('The person ID who logged the time'), project_id: z .union([z.string(), z.number()]) .optional() .describe('The project ID for the logged time'), task_id: z.string().optional().describe('The task ID for the logged time'), date: z.string().optional().describe('The date for the logged time (YYYY-MM-DD)'), hours: z.number().optional().describe('The number of hours logged'), billable: z .union([z.string(), z.number()]) .optional() .describe('Whether the time is billable (1 = billable, 0 = non-billable)'), notes: z.string().optional().describe('Optional notes describing the work done'), reference_date: z.string().optional().describe('Reference date for UI suggestions'), }), async (params) => { const { logged_time_id, ...updateData } = params; const loggedTime = await floatApi.patch( `/logged-time/${logged_time_id}`, updateData, loggedTimeSchema ); return loggedTime; } ); // Delete logged time entry export const deleteLoggedTime = createTool( 'delete-logged-time', 'Delete a logged time entry', z.object({ logged_time_id: z.string().describe('The logged time ID (hexadecimal)'), }), async (params) => { await floatApi.delete(`/logged-time/${params.logged_time_id}`); return { success: true, message: 'Logged time entry deleted successfully' }; } ); // Bulk create logged time entries export const bulkCreateLoggedTime = createTool( 'bulk-create-logged-time', 'Create multiple logged time entries at once for batch time logging', z.object({ logged_time_entries: z .array( z.object({ people_id: z .union([z.string(), z.number()]) .describe('The person ID who logged the time'), project_id: z .union([z.string(), z.number()]) .describe('The project ID for the logged time'), task_id: z.string().optional().describe('The task ID for the logged time'), date: z.string().describe('The date for the logged time (YYYY-MM-DD)'), hours: z.number().describe('The number of hours logged'), billable: z .union([z.string(), z.number()]) .optional() .describe('Whether the time is billable (1 = billable, 0 = non-billable)'), notes: z.string().optional().describe('Optional notes describing the work done'), reference_date: z.string().optional().describe('Reference date for UI suggestions'), }) ) .describe('Array of logged time entries to create'), }), async (params) => { const results = []; const errors = []; for (let index = 0; index < params.logged_time_entries.length; index++) { const entry = params.logged_time_entries[index]; try { const loggedTime = await floatApi.post('/logged-time', entry, loggedTimeSchema); results.push({ index, success: true, data: loggedTime }); } catch (error) { errors.push({ index, success: false, error: error instanceof Error ? error.message : 'Unknown error', entry, }); } } return { success: errors.length === 0, results, errors, summary: { total: params.logged_time_entries.length, successful: results.length, failed: errors.length, }, }; } ); // Get logged time summary by person export const getPersonLoggedTimeSummary = createTool( 'get-person-logged-time-summary', 'Get logged time summary for a specific person including billable/non-billable hours', z.object({ people_id: z.union([z.string(), z.number()]).describe('The person ID'), start_date: z.string().optional().describe('Start date for summary (YYYY-MM-DD)'), end_date: z.string().optional().describe('End date for summary (YYYY-MM-DD)'), project_id: z.union([z.string(), z.number()]).optional().describe('Filter by project ID'), billable: z .union([z.string(), z.number()]) .optional() .describe('Filter by billable status (1 = billable, 0 = non-billable)'), }), async (params) => { const loggedTimeData = await floatApi.getPaginated( '/logged-time', { people_id: params.people_id, start_date: params.start_date, end_date: params.end_date, project_id: params.project_id, billable: params.billable, }, loggedTimeResponseSchema ); const summary = { people_id: params.people_id, date_range: { start_date: params.start_date, end_date: params.end_date, }, total_hours: 0, billable_hours: 0, non_billable_hours: 0, by_project: {} as Record< string, { total_hours: number; billable_hours: number; non_billable_hours: number } >, by_date: {} as Record< string, { total_hours: number; billable_hours: number; non_billable_hours: number } >, entries: loggedTimeData, }; loggedTimeData.forEach((entry) => { const hours = entry.hours || 0; const isBillable = entry.billable === 1; // Total hours summary.total_hours += hours; // Billable vs non-billable if (isBillable) { summary.billable_hours += hours; } else { summary.non_billable_hours += hours; } // By project if (entry.project_id) { const projectId = entry.project_id.toString(); if (!summary.by_project[projectId]) { summary.by_project[projectId] = { total_hours: 0, billable_hours: 0, non_billable_hours: 0, }; } summary.by_project[projectId].total_hours += hours; if (isBillable) { summary.by_project[projectId].billable_hours += hours; } else { summary.by_project[projectId].non_billable_hours += hours; } } // By date if (entry.date) { if (!summary.by_date[entry.date]) { summary.by_date[entry.date] = { total_hours: 0, billable_hours: 0, non_billable_hours: 0, }; } summary.by_date[entry.date].total_hours += hours; if (isBillable) { summary.by_date[entry.date].billable_hours += hours; } else { summary.by_date[entry.date].non_billable_hours += hours; } } }); return summary; } ); // Get logged time summary by project export const getProjectLoggedTimeSummary = createTool( 'get-project-logged-time-summary', 'Get logged time summary for a specific project including team member contributions', z.object({ project_id: z.union([z.string(), z.number()]).describe('The project ID'), start_date: z.string().optional().describe('Start date for summary (YYYY-MM-DD)'), end_date: z.string().optional().describe('End date for summary (YYYY-MM-DD)'), people_id: z.union([z.string(), z.number()]).optional().describe('Filter by person ID'), billable: z .union([z.string(), z.number()]) .optional() .describe('Filter by billable status (1 = billable, 0 = non-billable)'), }), async (params) => { const loggedTimeData = await floatApi.getPaginated( '/logged-time', { project_id: params.project_id, start_date: params.start_date, end_date: params.end_date, people_id: params.people_id, billable: params.billable, }, loggedTimeResponseSchema ); const summary = { project_id: params.project_id, date_range: { start_date: params.start_date, end_date: params.end_date, }, total_hours: 0, billable_hours: 0, non_billable_hours: 0, by_person: {} as Record< string, { total_hours: number; billable_hours: number; non_billable_hours: number } >, by_date: {} as Record< string, { total_hours: number; billable_hours: number; non_billable_hours: number } >, entries: loggedTimeData, }; loggedTimeData.forEach((entry) => { const hours = entry.hours || 0; const isBillable = entry.billable === 1; // Total hours summary.total_hours += hours; // Billable vs non-billable if (isBillable) { summary.billable_hours += hours; } else { summary.non_billable_hours += hours; } // By person if (entry.people_id) { const peopleId = entry.people_id.toString(); if (!summary.by_person[peopleId]) { summary.by_person[peopleId] = { total_hours: 0, billable_hours: 0, non_billable_hours: 0, }; } summary.by_person[peopleId].total_hours += hours; if (isBillable) { summary.by_person[peopleId].billable_hours += hours; } else { summary.by_person[peopleId].non_billable_hours += hours; } } // By date if (entry.date) { if (!summary.by_date[entry.date]) { summary.by_date[entry.date] = { total_hours: 0, billable_hours: 0, non_billable_hours: 0, }; } summary.by_date[entry.date].total_hours += hours; if (isBillable) { summary.by_date[entry.date].billable_hours += hours; } else { summary.by_date[entry.date].non_billable_hours += hours; } } }); return summary; } ); // Get logged time timesheet view export const getLoggedTimeTimesheet = createTool( 'get-logged-time-timesheet', 'Get logged time in timesheet format for a specific date range, organized by person and date', z.object({ start_date: z.string().describe('Start date for timesheet (YYYY-MM-DD)'), end_date: z.string().describe('End date for timesheet (YYYY-MM-DD)'), people_id: z.union([z.string(), z.number()]).optional().describe('Filter by person ID'), project_id: z.union([z.string(), z.number()]).optional().describe('Filter by project ID'), billable: z .union([z.string(), z.number()]) .optional() .describe('Filter by billable status (1 = billable, 0 = non-billable)'), }), async (params) => { const loggedTimeData = await floatApi.getPaginated( '/logged-time', { start_date: params.start_date, end_date: params.end_date, people_id: params.people_id, project_id: params.project_id, billable: params.billable, }, loggedTimeResponseSchema ); // Group by person and date for timesheet view const timesheet: Record<string, Record<string, LoggedTime[]>> = {}; const totals = { total_hours: 0, billable_hours: 0, non_billable_hours: 0, }; loggedTimeData.forEach((entry) => { const peopleId = entry.people_id?.toString() || 'unknown'; const date = entry.date || 'unknown'; const hours = entry.hours || 0; const isBillable = entry.billable === 1; // Initialize structure if (!timesheet[peopleId]) { timesheet[peopleId] = {}; } if (!timesheet[peopleId][date]) { timesheet[peopleId][date] = []; } // Add entry timesheet[peopleId][date].push(entry); // Calculate totals totals.total_hours += hours; if (isBillable) { totals.billable_hours += hours; } else { totals.non_billable_hours += hours; } }); return { timesheet, totals, date_range: { start_date: params.start_date, end_date: params.end_date, }, total_entries: loggedTimeData.length, }; } ); // Get billable time report export const getBillableTimeReport = createTool( 'get-billable-time-report', 'Get a detailed report of billable vs non-billable logged time with breakdown by person and project', z.object({ start_date: z.string().describe('Start date for report (YYYY-MM-DD)'), end_date: z.string().describe('End date for report (YYYY-MM-DD)'), people_id: z.union([z.string(), z.number()]).optional().describe('Filter by person ID'), project_id: z.union([z.string(), z.number()]).optional().describe('Filter by project ID'), }), async (params) => { const loggedTimeData = await floatApi.getPaginated( '/logged-time', { start_date: params.start_date, end_date: params.end_date, people_id: params.people_id, project_id: params.project_id, }, loggedTimeResponseSchema ); const report = { date_range: { start_date: params.start_date, end_date: params.end_date, }, summary: { total_hours: 0, billable_hours: 0, non_billable_hours: 0, billable_percentage: 0, total_entries: loggedTimeData.length, }, by_person: {} as Record< string, { total_hours: number; billable_hours: number; non_billable_hours: number; billable_percentage: number; } >, by_project: {} as Record< string, { total_hours: number; billable_hours: number; non_billable_hours: number; billable_percentage: number; } >, }; loggedTimeData.forEach((entry) => { const hours = entry.hours || 0; const isBillable = entry.billable === 1; // Summary totals report.summary.total_hours += hours; if (isBillable) { report.summary.billable_hours += hours; } else { report.summary.non_billable_hours += hours; } // By person if (entry.people_id) { const peopleId = entry.people_id.toString(); if (!report.by_person[peopleId]) { report.by_person[peopleId] = { total_hours: 0, billable_hours: 0, non_billable_hours: 0, billable_percentage: 0, }; } report.by_person[peopleId].total_hours += hours; if (isBillable) { report.by_person[peopleId].billable_hours += hours; } else { report.by_person[peopleId].non_billable_hours += hours; } } // By project if (entry.project_id) { const projectId = entry.project_id.toString(); if (!report.by_project[projectId]) { report.by_project[projectId] = { total_hours: 0, billable_hours: 0, non_billable_hours: 0, billable_percentage: 0, }; } report.by_project[projectId].total_hours += hours; if (isBillable) { report.by_project[projectId].billable_hours += hours; } else { report.by_project[projectId].non_billable_hours += hours; } } }); // Calculate percentages if (report.summary.total_hours > 0) { report.summary.billable_percentage = (report.summary.billable_hours / report.summary.total_hours) * 100; } Object.values(report.by_person).forEach((person) => { if (person.total_hours > 0) { person.billable_percentage = (person.billable_hours / person.total_hours) * 100; } }); Object.values(report.by_project).forEach((project) => { if (project.total_hours > 0) { project.billable_percentage = (project.billable_hours / project.total_hours) * 100; } }); return report; } );

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/asachs01/float-mcp'

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