@savikko/tttui
Version:
Toggl Track TUI - A terminal user interface for Toggl Track
225 lines (190 loc) • 6.79 kB
text/typescript
import { API_BASE_URL } from './constants';
import { Workspace, Client, Project, TimeEntry } from './types';
export class TogglClient {
private baseUrl = API_BASE_URL;
private token: string;
constructor(token: string) {
this.token = token;
}
private async request<T>(
path: string,
options: {
method?: string;
body?: any;
} = {}
): Promise<T> {
const response = await fetch(`${this.baseUrl}${path}`, {
method: options.method || 'GET',
body: options.body ? JSON.stringify(options.body) : undefined,
headers: {
Authorization: `Basic ${Buffer.from(`${this.token}:api_token`).toString('base64')}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}, body: ${await response.text()}`);
}
return response.json();
}
async getWorkspaces(): Promise<Workspace[]> {
return this.request<Workspace[]>('/workspaces');
}
async getClients(workspaceId: number): Promise<Client[]> {
return this.request<Client[]>(`/workspaces/${workspaceId}/clients`);
}
async createClient(workspaceId: number, name: string): Promise<Client> {
return this.request<Client>(`/workspaces/${workspaceId}/clients`, {
method: 'POST',
body: { name },
});
}
async getProjects(workspaceId: number): Promise<Project[]> {
return this.request<Project[]>(`/workspaces/${workspaceId}/projects`);
}
async createProject(workspaceId: number, name: string, clientId: number): Promise<Project> {
return this.request<Project>(`/workspaces/${workspaceId}/projects`, {
method: 'POST',
body: {
name,
client_id: clientId,
active: true,
is_private: false,
},
});
}
async getProjectsByClient(workspaceId: number, clientId: number): Promise<Project[]> {
const projects = await this.getProjects(workspaceId);
return projects.filter((project) => project.client_id === clientId);
}
async getProjectDetails(
workspaceId: number,
projectId: number
): Promise<{ project: Project; client: Client | null }> {
const projects = await this.getProjects(workspaceId);
const project = projects.find((p) => p.id === projectId);
if (!project) {
throw new Error(`Project with ID ${projectId} not found`);
}
if (project.client_id) {
const clients = await this.getClients(workspaceId);
const client = clients.find((c) => c.id === project.client_id);
return { project, client: client || null };
}
return { project, client: null };
}
async getCurrentTimeEntry(): Promise<TimeEntry | null> {
try {
return await this.request<TimeEntry>('/me/time_entries/current');
} catch (error) {
if (error instanceof Error && error.message.includes('404')) {
return null;
}
throw error;
}
}
async startTimeEntry(timeEntry: {
description: string;
workspaceId: number;
projectId: number;
billable?: boolean;
}): Promise<TimeEntry> {
return this.request<TimeEntry>(`/workspaces/${timeEntry.workspaceId}/time_entries`, {
method: 'POST',
body: {
created_with: 'toggl-cli',
description: timeEntry.description,
project_id: timeEntry.projectId,
billable: timeEntry.billable,
start: new Date().toISOString(),
duration: -1, // Running time entry
wid: timeEntry.workspaceId,
},
});
}
async stopTimeEntry(workspaceId: number, timeEntryId: number): Promise<TimeEntry> {
return this.request<TimeEntry>(`/workspaces/${workspaceId}/time_entries/${timeEntryId}/stop`, {
method: 'PATCH',
});
}
async updateTimeEntryDescription(
workspaceId: number,
timeEntryId: number,
description: string
): Promise<TimeEntry> {
return this.request<TimeEntry>(`/workspaces/${workspaceId}/time_entries/${timeEntryId}`, {
method: 'PUT',
body: {
description,
},
});
}
async getRecentTimeEntries(workspaceId: number, projectId?: number): Promise<TimeEntry[]> {
const entries = await this.request<TimeEntry[]>('/me/time_entries');
if (projectId) {
return entries.filter((entry) => entry.project_id === projectId);
}
return entries;
}
async getRecentTimeEntriesWithDetails(
workspaceId: number,
limit: number = 10
): Promise<TimeEntry[]> {
// Get entries from the last month
const endDate = new Date();
const startDate = new Date();
startDate.setMonth(startDate.getMonth() - 1);
const entries = await this.request<TimeEntry[]>(`/me/time_entries`);
// Filter entries by workspace and date
const filteredEntries = entries.filter((entry) => {
const entryDate = new Date(entry.start);
return entry.workspace_id === workspaceId && entryDate >= startDate && entryDate <= endDate;
});
// Get all unique project IDs from entries
const projectIds = new Set(
filteredEntries.filter((e) => e.project_id).map((e) => e.project_id)
);
// Fetch all projects in one go
const projects = await this.getProjects(workspaceId);
// Get all unique client IDs from projects
const clientIds = new Set(projects.filter((p) => p.client_id).map((p) => p.client_id));
// Fetch all clients in one go
const clients = await this.getClients(workspaceId);
// Create lookup maps
const projectMap = new Map(projects.map((p) => [p.id, p]));
const clientMap = new Map(clients.map((c) => [c.id, c]));
// Sort entries by start date (newest first) and take the first 'limit' entries
const sortedEntries = filteredEntries
.sort((a, b) => new Date(b.start).getTime() - new Date(a.start).getTime())
.slice(0, limit);
// Enrich entries with project and client details
return sortedEntries.map((entry) => ({
...entry,
project: entry.project_id ? projectMap.get(entry.project_id) : undefined,
client: entry.project_id
? clientMap.get(projectMap.get(entry.project_id)?.client_id || 0)
: undefined,
}));
}
async updateTimeEntry(
workspaceId: number,
timeEntryId: number,
data: {
start?: string;
stop?: string;
description?: string;
}
): Promise<TimeEntry> {
return this.request<TimeEntry>(`/workspaces/${workspaceId}/time_entries/${timeEntryId}`, {
method: 'PUT',
body: data,
});
}
async getClientDetails(workspaceId: number, clientId: number): Promise<Client> {
const clients = await this.getClients(workspaceId);
const client = clients.find((c) => c.id === clientId);
if (!client) {
throw new Error(`Client with ID ${clientId} not found`);
}
return client;
}
}