import { z } from 'zod';
import { createTool, withFormatParam } from '../base.js';
import { floatApi } from '../../services/float-api.js';
import {
loggedTimeSchema,
loggedTimeResponseSchema,
timeOffSchema,
timeOffResponseSchema,
timeOffTypeSchema,
timeOffTypesResponseSchema,
publicHolidaySchema,
publicHolidaysResponseSchema,
teamHolidaySchema,
teamHolidaysResponseSchema,
type LoggedTime,
} from '../../types/float.js';
// Time tracking entity types enum for decision tree routing
const timeTrackingEntityTypeSchema = z.enum([
'logged-time',
'timeoff',
'timeoff-types',
'public-holidays',
'team-holidays',
]);
// Time tracking operation types enum for decision tree routing
const timeTrackingOperationTypeSchema = z.enum([
'list',
'get',
'create',
'update',
'delete',
// Logged time specific operations
'bulk-create-logged-time',
'get-person-logged-time-summary',
'get-project-logged-time-summary',
'get-logged-time-timesheet',
'get-billable-time-report',
// Time off specific operations
'bulk-create-timeoff',
'approve-timeoff',
'reject-timeoff',
'get-timeoff-calendar',
'get-person-timeoff-summary',
// Team holiday specific operations
'list-team-holidays-by-department',
'list-team-holidays-by-date-range',
'list-recurring-team-holidays',
'get-upcoming-team-holidays',
]);
// Base parameters for time tracking operations
const timeTrackingBaseParamsSchema = z.object({
entity_type: timeTrackingEntityTypeSchema.describe(
'The type of time tracking entity (logged-time, timeoff, timeoff-types, public-holidays, team-holidays)'
),
operation: timeTrackingOperationTypeSchema.describe('The time tracking operation to perform'),
});
// Generic list/filter parameters for time tracking
const timeTrackingListParamsSchema = 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)'),
end_date: z.string().optional().describe('Filter by end date (YYYY-MM-DD)'),
date: z.string().optional().describe('Filter by specific date (YYYY-MM-DD)'),
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)'),
status: z.string().optional().describe('Filter by status (pending, approved, rejected)'),
timeoff_type_id: z.number().optional().describe('Filter by time off type ID'),
department_id: z.number().optional().describe('Filter by department ID'),
region_id: z.number().optional().describe('Filter by region ID'),
recurring: z
.number()
.optional()
.describe('Filter by recurring status (0=one-time, 1=recurring)'),
active: z.number().optional().describe('Filter by active status (0=archived, 1=active)'),
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'),
})
.partial();
// Generic get parameters
const timeTrackingGetParamsSchema = z
.object({
id: z
.union([z.string(), z.number()])
.describe('The entity ID (logged_time_id, timeoff_id, timeoff_type_id, holiday_id)'),
})
.partial();
// Create/update data schema for time tracking entities
const timeTrackingCreateUpdateDataSchema = z
.object({
// Logged time fields
hours: z.number().optional().describe('Hours logged or time off hours'),
notes: z
.string()
.optional()
.describe('Notes or description for logged time entries or holidays'),
reference_date: z.string().optional().describe('Reference date for UI suggestions'),
// Time off fields
full_day: z.number().optional().describe('Full day flag (1=full day, 0=partial day)'),
approved_by: z.number().optional().describe('User ID who approved'),
approved_at: z.string().optional().describe('Approval timestamp'),
rejected_by: z.number().optional().describe('User ID who rejected'),
rejected_at: z.string().optional().describe('Rejection timestamp'),
repeat_state: z.number().optional().describe('Repeat state'),
repeat_end: z.string().optional().describe('Repeat end date'),
// Time off type fields
name: z.string().optional().describe('Name of the time off type or holiday'),
is_default: z.number().optional().describe('Default flag (0=not default, 1=default)'),
color: z.string().optional().describe('Color (hex code)'),
// Holiday fields (public and team)
description: z.string().optional().describe('Holiday description'),
region: z.string().optional().describe('Region or country code'),
country: z.string().optional().describe('Country name'),
type: z.string().optional().describe('Holiday type'),
moveable: z.number().optional().describe('Moveable flag (0=fixed, 1=moveable)'),
year: z.number().optional().describe('Year for the holiday'),
holiday_type: z.number().optional().describe('Holiday type (0=full day, 1=partial day)'),
recurrence_pattern: z.string().optional().describe('Recurrence pattern'),
created_by: z.number().optional().describe('User ID who created'),
all_day: z.number().optional().describe('All day flag (0=not all day, 1=all day)'),
timezone: z.string().optional().describe('Timezone'),
// Bulk operations
logged_time_entries: z
.array(
z.object({
people_id: z.union([z.string(), z.number()]),
project_id: z.union([z.string(), z.number()]),
task_id: z.string().optional(),
date: z.string(),
hours: z.number(),
billable: z.union([z.string(), z.number()]).optional(),
notes: z.string().optional(),
reference_date: z.string().optional(),
})
)
.optional()
.describe('Array of logged time entries for bulk creation'),
timeoff_entries: z
.array(
z.object({
people_id: z.number(),
timeoff_type_id: z.number(),
start_date: z.string(),
end_date: z.string(),
hours: z.number().optional(),
full_day: z.number().optional(),
notes: z.string().optional(),
})
)
.optional()
.describe('Array of time off entries for bulk creation'),
})
.partial();
// Main schema combining all parameters
const manageTimeTrackingSchema = withFormatParam(
timeTrackingBaseParamsSchema.extend({
...timeTrackingListParamsSchema.shape,
...timeTrackingGetParamsSchema.shape,
...timeTrackingCreateUpdateDataSchema.shape,
})
);
export const manageTimeTracking = createTool(
'manage-time-tracking',
'Consolidated tool for managing all time tracking entities (logged-time, timeoff, timeoff-types, public-holidays, team-holidays). Handles time logging, leave management, and holiday tracking through a decision-tree approach with comprehensive reporting capabilities.',
manageTimeTrackingSchema,
async (params) => {
const { format, entity_type, operation, id, ...restParams } = params;
// Route based on entity type and operation
switch (entity_type) {
case 'logged-time':
return handleLoggedTimeOperations(operation, { id, ...restParams }, format);
case 'timeoff':
return handleTimeOffOperations(operation, { id, ...restParams }, format);
case 'timeoff-types':
return handleTimeOffTypeOperations(operation, { id, ...restParams }, format);
case 'public-holidays':
return handlePublicHolidayOperations(operation, { id, ...restParams }, format);
case 'team-holidays':
return handleTeamHolidayOperations(operation, { id, ...restParams }, format);
default:
throw new Error(`Unsupported time tracking entity type: ${entity_type}`);
}
}
);
// Define proper parameter types based on our schemas
type TimeTrackingParams = z.infer<typeof timeTrackingListParamsSchema> &
z.infer<typeof timeTrackingGetParamsSchema> &
z.infer<typeof timeTrackingCreateUpdateDataSchema>;
type TimeTrackingFormat = 'json' | 'xml';
// Helper function to convert numeric status to string status
function convertStatusToString(status: unknown): 'pending' | 'approved' | 'rejected' | 'unknown' {
if (typeof status === 'number') {
switch (status) {
case 1:
return 'pending';
case 2:
return 'approved';
case 3:
return 'rejected';
default:
return 'unknown';
}
}
if (typeof status === 'string') {
if (status === 'pending' || status === 'approved' || status === 'rejected') {
return status;
}
}
return 'unknown';
}
// Logged time operations handler
async function handleLoggedTimeOperations(
operation: string,
params: TimeTrackingParams,
format: TimeTrackingFormat
): Promise<unknown> {
const { id, logged_time_entries, people_id, project_id, ...otherParams } = params;
switch (operation) {
case 'list':
return floatApi.getPaginated('/logged-time', otherParams, loggedTimeResponseSchema, format);
case 'get':
return floatApi.get(`/logged-time/${id}`, loggedTimeSchema, format);
case 'create':
return floatApi.post('/logged-time', otherParams, loggedTimeSchema, format);
case 'update':
return floatApi.patch(`/logged-time/${id}`, otherParams, loggedTimeSchema, format);
case 'delete':
await floatApi.delete(`/logged-time/${id}`, undefined, format);
return { success: true, message: 'Logged time entry deleted successfully' };
case 'bulk-create-logged-time': {
const results = [];
const errors = [];
const entries = logged_time_entries || [];
for (let index = 0; index < entries.length; index++) {
const entry = entries[index];
try {
const loggedTime = await floatApi.post('/logged-time', entry, loggedTimeSchema, format);
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: entries.length,
successful: results.length,
failed: errors.length,
},
};
}
case 'get-person-logged-time-summary':
if (!people_id) throw new Error('people_id is required for person logged time summary');
return generatePersonLoggedTimeSummary(people_id, otherParams, format);
case 'get-project-logged-time-summary':
if (!project_id) throw new Error('project_id is required for project logged time summary');
return generateProjectLoggedTimeSummary(project_id, otherParams, format);
case 'get-logged-time-timesheet':
return generateLoggedTimeTimesheet(otherParams, format);
case 'get-billable-time-report':
return generateBillableTimeReport(otherParams, format);
default:
throw new Error(`Unsupported logged time operation: ${operation}`);
}
}
// Time off operations handler
async function handleTimeOffOperations(
operation: string,
params: TimeTrackingParams,
format: TimeTrackingFormat
): Promise<unknown> {
const { id, timeoff_entries, ...otherParams } = params;
switch (operation) {
case 'list':
return floatApi.getPaginated('/timeoffs', otherParams, timeOffResponseSchema, format);
case 'get':
return floatApi.get(`/timeoffs/${id}`, timeOffSchema, format);
case 'create':
return floatApi.post('/timeoffs', otherParams, timeOffSchema, format);
case 'update':
return floatApi.patch(`/timeoffs/${id}`, otherParams, timeOffSchema, format);
case 'delete':
await floatApi.delete(`/timeoffs/${id}`, undefined, format);
return { success: true, message: 'Time off entry deleted successfully' };
case 'bulk-create-timeoff': {
const results = [];
const errors = [];
const entries = timeoff_entries || [];
for (let index = 0; index < entries.length; index++) {
const entry = entries[index];
try {
const timeOff = await floatApi.post('/timeoffs', entry, timeOffSchema, format);
results.push({ index, success: true, data: timeOff });
} 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: entries.length,
successful: results.length,
failed: errors.length,
},
};
}
case 'approve-timeoff': {
const approver_id = otherParams.approved_by || 1; // Default to system user
return floatApi.patch(
`/timeoffs/${id}`,
{
status: 2, // 2 = approved (numeric status)
approved_by: approver_id,
approved_at: new Date().toISOString(),
},
timeOffSchema,
format
);
}
case 'reject-timeoff': {
const rejector_id = otherParams.rejected_by || 1; // Default to system user
return floatApi.patch(
`/timeoffs/${id}`,
{
status: 3, // 3 = rejected (numeric status)
rejected_by: rejector_id,
rejected_at: new Date().toISOString(),
},
timeOffSchema,
format
);
}
case 'get-timeoff-calendar':
return generateTimeOffCalendar(otherParams, format);
case 'get-person-timeoff-summary':
if (!params.people_id) throw new Error('people_id is required for person time-off summary');
return generatePersonTimeOffSummary(params.people_id, otherParams, format);
default:
throw new Error(`Unsupported time off operation: ${operation}`);
}
}
// Time off type operations handler
async function handleTimeOffTypeOperations(
operation: string,
params: TimeTrackingParams,
format: TimeTrackingFormat
): Promise<unknown> {
const { id, ...otherParams } = params;
switch (operation) {
case 'list':
return floatApi.getPaginated(
'/timeoff-types',
otherParams,
timeOffTypesResponseSchema,
format
);
case 'get':
return floatApi.get(`/timeoff-types/${id}`, timeOffTypeSchema, format);
case 'create':
return floatApi.post('/timeoff-types', otherParams, timeOffTypeSchema, format);
case 'update':
return floatApi.patch(`/timeoff-types/${id}`, otherParams, timeOffTypeSchema, format);
case 'delete':
await floatApi.delete(`/timeoff-types/${id}`, undefined, format);
return { success: true, message: 'Time off type deleted successfully' };
default:
throw new Error(`Unsupported time off type operation: ${operation}`);
}
}
// Public holiday operations handler
async function handlePublicHolidayOperations(
operation: string,
params: TimeTrackingParams,
format: TimeTrackingFormat
): Promise<unknown> {
const { id, ...otherParams } = params;
switch (operation) {
case 'list':
return floatApi.getPaginated(
'/public-holidays',
otherParams,
publicHolidaysResponseSchema,
format
);
case 'get':
return floatApi.get(`/public-holidays/${id}`, publicHolidaySchema, format);
case 'create':
return floatApi.post('/public-holidays', otherParams, publicHolidaySchema, format);
case 'update':
return floatApi.patch(`/public-holidays/${id}`, otherParams, publicHolidaySchema, format);
case 'delete':
await floatApi.delete(`/public-holidays/${id}`, undefined, format);
return { success: true, message: 'Public holiday deleted successfully' };
default:
throw new Error(`Unsupported public holiday operation: ${operation}`);
}
}
// Team holiday operations handler
async function handleTeamHolidayOperations(
operation: string,
params: TimeTrackingParams,
format: TimeTrackingFormat
): Promise<unknown> {
const { id, department_id, start_date, end_date, ...otherParams } = params;
switch (operation) {
case 'list':
return floatApi.getPaginated(
'/team-holidays',
otherParams,
teamHolidaysResponseSchema,
format
);
case 'get':
return floatApi.get(`/team-holidays/${id}`, teamHolidaySchema, format);
case 'create':
return floatApi.post('/team-holidays', otherParams, teamHolidaySchema, format);
case 'update':
return floatApi.patch(`/team-holidays/${id}`, otherParams, teamHolidaySchema, format);
case 'delete':
await floatApi.delete(`/team-holidays/${id}`, undefined, format);
return { success: true, message: 'Team holiday deleted successfully' };
case 'list-team-holidays-by-department':
return floatApi.getPaginated(
'/team-holidays',
{ department_id },
teamHolidaysResponseSchema,
format
);
case 'list-team-holidays-by-date-range':
return floatApi.getPaginated(
'/team-holidays',
{ start_date, end_date },
teamHolidaysResponseSchema,
format
);
case 'list-recurring-team-holidays':
return floatApi.getPaginated(
'/team-holidays',
{ recurring: 1 },
teamHolidaysResponseSchema,
format
);
case 'get-upcoming-team-holidays': {
const today = new Date().toISOString().split('T')[0];
const upcomingHolidays = await floatApi.getPaginated(
'/team-holidays',
{
start_date: today,
active: 1,
},
teamHolidaysResponseSchema,
format
);
return upcomingHolidays.sort((a, b) => {
const dateA = new Date(a.start_date).getTime();
const dateB = new Date(b.start_date).getTime();
return dateA - dateB;
});
}
default:
throw new Error(`Unsupported team holiday operation: ${operation}`);
}
}
// Helper functions for complex operations
async function generatePersonLoggedTimeSummary(
people_id: string | number,
params: TimeTrackingParams,
format: TimeTrackingFormat
): Promise<Record<string, unknown>> {
const loggedTimeData = await floatApi.getPaginated(
'/logged-time',
{
people_id,
...params,
},
loggedTimeResponseSchema,
format
);
const summary = {
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;
summary.total_hours += hours;
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;
}
async function generateProjectLoggedTimeSummary(
project_id: string | number,
params: TimeTrackingParams,
format: TimeTrackingFormat
): Promise<Record<string, unknown>> {
const loggedTimeData = await floatApi.getPaginated(
'/logged-time',
{
project_id,
...params,
},
loggedTimeResponseSchema,
format
);
const summary = {
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;
summary.total_hours += hours;
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;
}
async function generateLoggedTimeTimesheet(
params: TimeTrackingParams,
format: TimeTrackingFormat
): Promise<Record<string, unknown>> {
const loggedTimeData = await floatApi.getPaginated(
'/logged-time',
params,
loggedTimeResponseSchema,
format
);
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;
if (!timesheet[peopleId]) {
timesheet[peopleId] = {};
}
if (!timesheet[peopleId][date]) {
timesheet[peopleId][date] = [];
}
timesheet[peopleId][date].push(entry);
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,
};
}
async function generateBillableTimeReport(
params: TimeTrackingParams,
format: TimeTrackingFormat
): Promise<Record<string, unknown>> {
const loggedTimeData = await floatApi.getPaginated(
'/logged-time',
params,
loggedTimeResponseSchema,
format
);
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, unknown>,
by_project: {} as Record<string, unknown>,
};
loggedTimeData.forEach((entry) => {
const hours = entry.hours || 0;
const isBillable = entry.billable === 1;
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] as Record<string, unknown>).total_hours =
((report.by_person[peopleId] as Record<string, unknown>).total_hours as number) + hours;
if (isBillable) {
(report.by_person[peopleId] as Record<string, unknown>).billable_hours =
((report.by_person[peopleId] as Record<string, unknown>).billable_hours as number) +
hours;
} else {
(report.by_person[peopleId] as Record<string, unknown>).non_billable_hours =
((report.by_person[peopleId] as Record<string, unknown>).non_billable_hours as number) +
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] as Record<string, unknown>).total_hours =
((report.by_project[projectId] as Record<string, unknown>).total_hours as number) + hours;
if (isBillable) {
(report.by_project[projectId] as Record<string, unknown>).billable_hours =
((report.by_project[projectId] as Record<string, unknown>).billable_hours as number) +
hours;
} else {
(report.by_project[projectId] as Record<string, unknown>).non_billable_hours =
((report.by_project[projectId] as Record<string, unknown>).non_billable_hours as number) +
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) => {
const personRecord = person as Record<string, unknown>;
if ((personRecord.total_hours as number) > 0) {
personRecord.billable_percentage =
((personRecord.billable_hours as number) / (personRecord.total_hours as number)) * 100;
}
});
Object.values(report.by_project).forEach((project) => {
const projectRecord = project as Record<string, unknown>;
if ((projectRecord.total_hours as number) > 0) {
projectRecord.billable_percentage =
((projectRecord.billable_hours as number) / (projectRecord.total_hours as number)) * 100;
}
});
return report;
}
async function generateTimeOffCalendar(
params: TimeTrackingParams,
format: TimeTrackingFormat
): Promise<Record<string, unknown>> {
const timeOffData = await floatApi.getPaginated(
'/timeoffs',
params,
timeOffResponseSchema,
format
);
const calendar: Record<string, unknown[]> = {};
const summary = {
total_entries: timeOffData.length,
by_status: { pending: 0, approved: 0, rejected: 0 },
by_type: {} as Record<string, number>,
};
timeOffData.forEach((entry) => {
const startDate = entry.start_date || 'unknown';
if (!calendar[startDate]) {
calendar[startDate] = [];
}
calendar[startDate].push(entry);
// Status summary
if (entry.status) {
const statusString = convertStatusToString(entry.status);
if (statusString !== 'unknown') {
summary.by_status[statusString] = (summary.by_status[statusString] || 0) + 1;
}
}
// Type summary
const typeId = entry.timeoff_type_id?.toString() || 'unknown';
summary.by_type[typeId] = (summary.by_type[typeId] || 0) + 1;
});
return {
calendar,
summary,
date_range: {
start_date: params.start_date,
end_date: params.end_date,
},
};
}
async function generatePersonTimeOffSummary(
people_id: string | number,
params: TimeTrackingParams,
format: TimeTrackingFormat
): Promise<Record<string, unknown>> {
const timeOffData = await floatApi.getPaginated(
'/timeoffs',
{
people_id,
...params,
},
timeOffResponseSchema,
format
);
const summary = {
people_id,
date_range: {
start_date: params.start_date,
end_date: params.end_date,
},
total_days: 0,
total_hours: 0,
by_type: {} as Record<string, { days: number; hours: number }>,
by_status: { pending: 0, approved: 0, rejected: 0 },
entries: timeOffData,
};
timeOffData.forEach((entry) => {
const hours = entry.hours || (entry.full_day ? 8 : 0); // Assume 8 hours for full day
const days = entry.full_day ? 1 : hours / 8;
summary.total_hours += hours;
summary.total_days += days;
// By type
const typeId = entry.timeoff_type_id?.toString() || 'unknown';
if (!summary.by_type[typeId]) {
summary.by_type[typeId] = { days: 0, hours: 0 };
}
summary.by_type[typeId].days += days;
summary.by_type[typeId].hours += hours;
// By status
if (entry.status) {
const statusString = convertStatusToString(entry.status);
if (statusString !== 'unknown') {
summary.by_status[statusString] = (summary.by_status[statusString] || 0) + 1;
}
}
});
return summary;
}