{
  "id": "LLC5sbrvJwsCxFXq",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "n8n template - toggle raport with projects",
  "tags": [
    "toggle"
  ],
  "nodes": [
    {
      "id": "2632242c-671b-4e26-a9f4-534ecf3cfea6",
      "name": "Get Toggle Projects",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -304,
        -128
      ],
      "parameters": {
        "url": "https://api.track.toggl.com/api/v9/workspaces/TOGGL_WORKSPACE_ID/projects",
        "options": {
          "pagination": {
            "pagination": {
              "parameters": {
                "parameters": [
                  {
                    "name": "page",
                    "value": "={{ $pageCount + 1 }}"
                  }
                ]
              }
            }
          }
        },
        "sendQuery": true,
        "authentication": "predefinedCredentialType",
        "queryParameters": {
          "parameters": [
            {}
          ]
        },
        "nodeCredentialType": "togglApi"
      },
      "credentials": {
        "togglApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "60dd3732-4a0f-4544-b52e-1b2b2cb969cb",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -704,
        -240
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "months",
              "triggerAtHour": 4
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "63830937-0d2e-4ac2-bf8b-037483deba94",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -496,
        -560
      ],
      "parameters": {
        "color": 4,
        "width": 662,
        "height": 602,
        "content": "## Fetch Toggl clients, projects, and summary data form last month via HTTP nodes\n\n1. Enter your TOGGL_WORKSPACE_ID"
      },
      "typeVersion": 1
    },
    {
      "id": "871b782a-06c8-464f-abe0-6cfbe93dfe3d",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        176,
        -560
      ],
      "parameters": {
        "color": 4,
        "width": 310,
        "height": 602,
        "content": "## Merge data to associate projects with clients.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "43d03168-970c-44a1-93ac-6a1d0289def2",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        496,
        -560
      ],
      "parameters": {
        "color": 4,
        "width": 294,
        "height": 602,
        "content": "## Generate an HTML report with hours per client and per project\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "66c498df-8b6d-4ea7-b717-968411b1e918",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -800,
        -560
      ],
      "parameters": {
        "color": 4,
        "width": 294,
        "height": 602,
        "content": "## Schedule automation (monthly)\n"
      },
      "typeVersion": 1
    },
    {
      "id": "8db1e677-00e5-416d-b0c8-97faf5725149",
      "name": "README",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1232,
        -784
      ],
      "parameters": {
        "width": 416,
        "height": 832,
        "content": "## Description\nThis workflow fetches Toggl Track summary data for the previous month, aggregates hours per client and project, and emails a clean HTML report via Resend.\n\n## How it works\n1) Compute previous month period.\n2) Fetch Toggl summary (grouping=clients, sub_grouping=projects).\n3) Fetch clients and projects for proper names.\n4) Merge and aggregate seconds to hours.\n5) Generate HTML raport \n6) Send raport via Resend API.\n\n## Requirements\n- [Toggl free account](https://accounts.toggl.com/track/signup/) (Login, Pass, TOGGL_WORKSPACE_ID)\n- Resend.com free account RESEND_API_KEY\n\n## Customization\n- Change trigger time in the Schedule Trigger node.\n- Modify period calculation for weekly/quarterly in Get Toggle Summary node.\n- Add archived projects by querying with active=false&archived=true and merging.\n\n## Documentation\n- [Toggle docs](https://engineering.toggl.com/docs/)\n- [Resend.com docs](https://resend.com/docs/)\n\n## Author\nKrystian Syryca - [krsweb.pl](https://krsweb.pl)\n"
      },
      "typeVersion": 1
    },
    {
      "id": "3aa03862-f0a0-40e7-9d68-20d656a0d304",
      "name": "Get Toggle Summary",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -304,
        -352
      ],
      "parameters": {
        "url": "https://api.track.toggl.com/reports/api/v3/workspace/TOGGL_WORKSPACE_ID/summary/time_entries",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "sendHeaders": true,
        "authentication": "predefinedCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "start_date",
              "value": "={{ $now.minus({ months: 1 }).startOf('month').toFormat('yyyy-MM-dd') }}"
            },
            {
              "name": "end_date",
              "value": "={{ $now.minus({ months: 1 }).endOf('month').toFormat('yyyy-MM-dd') }}"
            },
            {
              "name": "grouping",
              "value": "clients"
            },
            {
              "name": "sub_grouping",
              "value": "projects"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "togglApi"
      },
      "credentials": {
        "togglApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "6d86fedc-c291-43dd-b008-1e0395d48d9f",
      "name": "Prepare html raport",
      "type": "n8n-nodes-base.code",
      "position": [
        576,
        -240
      ],
      "parameters": {
        "jsCode": "const data = $input.all().map(i => i.json);\nconst period = $json.period || \"Previous month\";\n\n// Group by clinet\nconst clients = {};\nfor (const i of data) {\n  const clientId = i.client_id || i.cid;\n  const clientName = i.client_name || \"(missing customer name)\";\n  if (!clients[clientId]) {\n    clients[clientId] = { name: clientName, projects: [] };\n  }\n  clients[clientId].projects.push({\n    project_id: i.project_id,\n    project_name: i.name || \"(bez nazwy projektu)\",\n    seconds: Number(i.seconds) || 0\n  });\n}\n\n// Sum clients time\nfor (const c of Object.values(clients)) {\n  c.totalSeconds = c.projects.reduce((a, p) => a + p.seconds, 0);\n}\n\n// Sort clients\nconst sortedClients = Object.entries(clients)\n  .sort(([, a], [, b]) => b.totalSeconds - a.totalSeconds);\n\n// Generate html raport\nlet totalSeconds = 0;\nlet html = \"<html><body>\";\nhtml += \"<h3>Time Report per Client and Project - \" + period + \"</h3>\";\n\nfor (const [clientId, c] of sortedClients) {\n  const clientHours = (c.totalSeconds / 3600).toFixed(2);\n  totalSeconds += c.totalSeconds;\n\n  html += \"<p><strong>\" + c.name + \"</strong> \u2013 \" + clientHours + \" h</p>\";\n  html += \"<ul>\";\n\n  const sortedProjects = c.projects.sort((a, b) => b.seconds - a.seconds);\n  for (const p of sortedProjects) {\n    const hours = (p.seconds / 3600).toFixed(2);\n    html += \"<li>\" + p.project_name + \" \u2013 \" + hours + \" h</li>\";\n  }\n\n  html += \"</ul>\";\n}\n\nhtml += \"<p><strong>Total hours: \" + (totalSeconds / 3600).toFixed(2) + \"</strong></p>\";\nhtml += \"</body></html>\";\n\nreturn [{\n  json: {\n    subject: \"Time Report per Client and Project - \" + period,\n    html\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "90949ac4-fd1d-4321-a9e1-c985a23e7b12",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        800,
        -560
      ],
      "parameters": {
        "color": 4,
        "width": 294,
        "height": 602,
        "content": "## Send the report via Resend email API.\n1. Enter RESEND_API_KEY\n2. Enter FROM and TO email addresses\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "3cf0af49-6e80-4dbc-8395-9861ed371a69",
      "name": "Send email via Resend",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        864,
        -240
      ],
      "parameters": {
        "url": "https://api.resend.com/emails",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"from\": \"<noreplay@yourdomain.com>\",\n  \"to\": [\"to@yourdomain.com\"],\n  \"subject\": \"{{$json.subject}}\",\n  \"html\": \"{{$json.html}}\"\n}\n",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "be2604f7-a5ca-46fe-b711-f53a81b1363e",
      "name": "Merge data",
      "type": "n8n-nodes-base.merge",
      "position": [
        288,
        -240
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "advanced": true,
        "joinMode": "enrichInput1",
        "mergeByFields": {
          "values": [
            {
              "field1": "project_id",
              "field2": "id"
            }
          ]
        }
      },
      "typeVersion": 3.2
    },
    {
      "id": "29d8af43-2951-4c9e-80de-556f65588a65",
      "name": "Prepare projects list",
      "type": "n8n-nodes-base.code",
      "position": [
        -80,
        -352
      ],
      "parameters": {
        "jsCode": "const groups = $json.groups || [];\nconst result = [];\n\nfor (const g of groups) {\n  const clientId = Number(g.id);\n  const subs = g.sub_groups || [];\n  for (const s of subs) {\n    result.push({\n      json: {\n        client_id: clientId,\n        project_id: Number(s.id),\n        seconds: Number(s.seconds) || 0\n      }\n    });\n  }\n}\n\nreturn result;\n"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "873a699b-08da-4b2e-b0af-66c2cd539290",
  "connections": {
    "Merge data": {
      "main": [
        [
          {
            "node": "Prepare html raport",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Get Toggle Summary",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get Toggle Projects",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Toggle Summary": {
      "main": [
        [
          {
            "node": "Prepare projects list",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Toggle Projects": {
      "main": [
        [
          {
            "node": "Merge data",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Prepare html raport": {
      "main": [
        [
          {
            "node": "Send email via Resend",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare projects list": {
      "main": [
        [
          {
            "node": "Merge data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}