diff --git a/api/toggl.js b/api/toggl.js new file mode 100644 index 0000000..49f6c50 --- /dev/null +++ b/api/toggl.js @@ -0,0 +1,43 @@ +import fetch from "node-fetch"; +import { Buffer } from "buffer"; +import fs from "fs"; +import path from "path"; +import os from "os"; + +const filePath = path.join(os.homedir(), ".toggl2tsc"); +const token = fs.readFileSync(filePath, "utf8"); +const base64Credentials = Buffer.from(`${token}:api_token`).toString("base64"); + +async function fetchFromToggl(url) { + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Basic ${base64Credentials}`, + }, + }); + + if (!response.ok) { + console.error( + `Failed to fetch data from Toggl API: ${response.statusText}`, + ); + return null; + } + + return await response.json(); +} + +export async function fetchTimeEntries(start, end) { + const url = `https://api.track.toggl.com/api/v9/me/time_entries?start_date=${start}&end_date=${end}`; + return await fetchFromToggl(url); +} + +export async function fetchWorkspaces() { + const url = `https://api.track.toggl.com/api/v9/workspaces`; + return await fetchFromToggl(url); +} + +export async function fetchProjects(workspaceId) { + const url = `https://api.track.toggl.com/api/v9/workspaces/${workspaceId}/projects`; + return await fetchFromToggl(url); +} diff --git a/commands/list.js b/commands/list.js index 80cf649..38bc37b 100644 --- a/commands/list.js +++ b/commands/list.js @@ -1,139 +1,46 @@ import chalk from "chalk"; -import fetch from "node-fetch"; -import { Buffer } from "buffer"; -import dayjs from "dayjs"; -import utc from "dayjs/plugin/utc.js"; -import timezone from "dayjs/plugin/timezone.js"; -import isBetween from "dayjs/plugin/isBetween.js"; -import fs from "fs"; -import path from "path"; -import os from "os"; -import createPrompt from "prompt-sync"; -import { program } from "commander"; - -const prompt = createPrompt({}); - -dayjs.extend(utc); -dayjs.extend(timezone); -dayjs.extend(isBetween); - -const filePath = path.join(os.homedir(), ".toggl2tsc"); - -const token = fs.readFileSync(filePath, "utf8"); - -const base64Credentials = Buffer.from(`${token}:api_token`).toString("base64"); - -const currTz = "Europe/Brussels"; -const today = dayjs().tz(currTz).startOf("day").toISOString(); -const tomorrow = dayjs().add(1, "days").tz(currTz).startOf("day").toISOString(); - -async function fetchTimeEntries(url) { - const timeEntriesResponse = await fetch(url, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Basic ${base64Credentials}`, - }, - }); - - if (!timeEntriesResponse.ok) { - console.error("Failed to fetch data from Toggl API"); - return []; - } - - return await timeEntriesResponse.json(); -} - -async function fetchWorkspaces() { - const workspacesUrl = `https://api.track.toggl.com/api/v9/workspaces`; - - const workspacesResponse = await fetch(workspacesUrl, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Basic ${base64Credentials}`, - }, - }); - - if (!workspacesResponse.ok) { - console.error("Failed to fetch workspaces from Toggl API"); - return []; - } - - return await workspacesResponse.json(); -} - -async function selectWorkspaceId() { - const workspaces = await fetchWorkspaces(); - - if (workspaces.length === 0) { - console.error("No workspaces found"); - return null; - } - - console.log(chalk.green("Select a workspace ID:")); - workspaces.forEach((workspace, index) => { - console.log(chalk.blueBright(`${index + 1}. ${workspace.name}`)); - }); - - const userInput = prompt( - "Enter the number corresponding to the workspace ID: ", - ); - const selectedIndex = parseInt(userInput); - - if ( - isNaN(selectedIndex) || - selectedIndex < 1 || - selectedIndex > workspaces.length - ) { - console.error("Invalid selection"); - return null; - } - - return workspaces[selectedIndex - 1].id; -} +import { fetchTimeEntries, fetchProjects } from "../api/toggl.js"; +import { + getStartOfDay, + getEndOfDay, + getCurrentDay, + getNextDay, + isBetweenDates, + getEndOfToday, +} from "../utils/dateUtils.js"; +import { selectWorkspaceId } from "../utils/promptUtils.js"; export async function list(options) { const { startDate, endDate, date } = options; - const workspaceId = await selectWorkspaceId(); - if (!workspaceId) { - return; - } - - // Determine the date range based on the provided options let start, end; if (date) { - start = dayjs(date).tz(currTz).startOf("day").toISOString(); - end = dayjs(date).tz(currTz).add(1, "day").startOf("day").toISOString(); + start = getStartOfDay(date); + end = getEndOfDay(date); + } else if (startDate && endDate) { + start = getStartOfDay(startDate); + end = getEndOfDay(endDate); + } else if (startDate) { + start = getStartOfDay(startDate); + end = endDate ? getEndOfDay(endDate) : getEndOfToday(); + } else if (!startDate && endDate) { + console.error(chalk.red("Please provide a start date")); + return; } else { - start = startDate - ? dayjs(startDate).tz(currTz).startOf("day").toISOString() - : today; - end = endDate - ? dayjs(endDate).tz(currTz).startOf("day").toISOString() - : tomorrow; + start = getCurrentDay(); + end = getNextDay(); } - const timeEntriesUrl = `https://api.track.toggl.com/api/v9/me/time_entries?start_date=${start}&end_date=${end}`; - const timeEntriesJson = await fetchTimeEntries(timeEntriesUrl); - - const projectsUrl = `https://api.track.toggl.com/api/v9/workspaces/${workspaceId}/projects`; - - const projectsResponse = await fetch(projectsUrl, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Basic ${base64Credentials}`, - }, - }); - - if (!projectsResponse.ok) { - console.error("Failed to fetch data from Toggl API"); + const workspaceId = await selectWorkspaceId(); + if (!workspaceId) { return; } - const projectsJson = await projectsResponse.json(); + const timeEntriesJson = await fetchTimeEntries(start, end); + if (!timeEntriesJson) return; + + const projectsJson = await fetchProjects(workspaceId); + if (!projectsJson) return; const validTimeEntries = timeEntriesJson.filter( (entry) => entry.stop !== null, @@ -142,11 +49,7 @@ export async function list(options) { const groupedEntries = validTimeEntries.reduce((acc, entry) => { const projectId = entry.project_id; if (!acc[projectId]) { - acc[projectId] = { - name: null, - entries: {}, - totalHours: 0, - }; + acc[projectId] = { entries: {}, totalHours: 0 }; } if (!acc[projectId].entries[entry.description]) { acc[projectId].entries[entry.description] = { @@ -159,10 +62,10 @@ export async function list(options) { return acc; }, {}); - const projectNamesMap = {}; - projectsJson.forEach((project) => { - projectNamesMap[project.id] = project.name; - }); + const projectNamesMap = projectsJson.reduce((map, project) => { + map[project.id] = project.name; + return map; + }, {}); console.log(chalk.green("Your current time entries:")); console.log(); @@ -173,7 +76,6 @@ export async function list(options) { console.log(chalk.green(`${projectName}`)); console.log(chalk.green("+".repeat(projectName.length))); - console.log( chalk.white(`Total hours: ${projectData.totalHours.toFixed(2)}`), ); @@ -191,7 +93,7 @@ export async function list(options) { }); const filteredEntries = validTimeEntries.filter((entry) => - dayjs(entry.start).isBetween(start, end), + isBetweenDates(entry, start, end), ); const totalHours = filteredEntries.reduce( (acc, entry) => acc + entry.duration / 3600, diff --git a/utils/dateUtils.js b/utils/dateUtils.js new file mode 100644 index 0000000..ea67443 --- /dev/null +++ b/utils/dateUtils.js @@ -0,0 +1,34 @@ +import dayjs from "dayjs"; +import utc from "dayjs/plugin/utc.js"; +import timezone from "dayjs/plugin/timezone.js"; +import isBetween from "dayjs/plugin/isBetween.js"; + +dayjs.extend(utc); +dayjs.extend(timezone); +dayjs.extend(isBetween); + +const currTz = "Europe/Brussels"; + +export function getCurrentDay() { + return dayjs().tz(currTz).startOf("day").toISOString(); +} + +export function getNextDay() { + return dayjs().add(1, "days").tz(currTz).startOf("day").toISOString(); +} + +export function getStartOfDay(date) { + return dayjs(date).tz(currTz).startOf("day").toISOString(); +} + +export function getEndOfDay(date) { + return dayjs(date).tz(currTz).endOf("day").toISOString(); +} + +export function getEndOfToday() { + return dayjs().tz(currTz).endOf("day").toISOString(); +} + +export function isBetweenDates(entry, start, end) { + return dayjs(entry.start).isBetween(start, end); +} diff --git a/utils/promptUtils.js b/utils/promptUtils.js new file mode 100644 index 0000000..9253618 --- /dev/null +++ b/utils/promptUtils.js @@ -0,0 +1,44 @@ +import chalk from "chalk"; +import createPrompt from "prompt-sync"; +import { fetchWorkspaces } from "../api/toggl.js"; + +const prompt = createPrompt({}); + +export async function selectWorkspaceId() { + const workspaces = await fetchWorkspaces(); + + if (!workspaces || workspaces.length === 0) { + console.error("No workspaces found"); + return null; + } + + if (workspaces.length === 1) { + console.log( + chalk.green( + `Automatically selecting the only workspace: ${chalk.blue(workspaces[0].name)}`, + ), + ); + return workspaces[0].id; + } + + console.log(chalk.green("Select a workspace ID:")); + workspaces.forEach((workspace, index) => { + console.log(chalk.blueBright(`${index + 1}. ${workspace.name}`)); + }); + + const userInput = prompt( + "Enter the number corresponding to the workspace ID: ", + ); + const selectedIndex = parseInt(userInput); + + if ( + isNaN(selectedIndex) || + selectedIndex < 1 || + selectedIndex > workspaces.length + ) { + console.error("Invalid selection"); + return null; + } + + return workspaces[selectedIndex - 1].id; +}