{
  "nodes": [
    {
      "id": "aac23c1f-e615-4b0d-984c-377700019233",
      "name": "Run Once a Week",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -2528,
        656
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                5
              ],
              "triggerAtHour": 16
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "0797665f-5803-4378-9098-e4ac6ec914bd",
      "name": "Get TMetric Users",
      "type": "n8n-nodes-base.httpRequest",
      "maxTries": 5,
      "position": [
        -1696,
        464
      ],
      "parameters": {
        "url": "={{$('Globals').item.json.apiBaseUrl}}/accounts/{{ $('Globals').item.json[\"tmAccountId\"] }}/reports/projects/filter",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "a94e162d-14ba-4a09-8dfe-fa7bdde9e7ff",
      "name": "Split Users Data",
      "type": "n8n-nodes-base.itemLists",
      "position": [
        -1440,
        464
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "users"
      },
      "typeVersion": 3
    },
    {
      "id": "482deb18-fdc2-49d4-aefe-754e978dd972",
      "name": "Process Each User Separately",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -288,
        496
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "7cf6836a-c937-4873-a4a0-86b045107b58",
      "name": "Get TMetric User Work Schedule",
      "type": "n8n-nodes-base.httpRequest",
      "maxTries": 5,
      "position": [
        -1696,
        640
      ],
      "parameters": {
        "url": "={{$('Globals').item.json.apiBaseUrl}}/accounts/{{ $('Globals').item.json.tmAccountId }}/schedule",
        "options": {},
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "startDate",
              "value": "={{ $('Globals').item.json.beginningOfTheMonth }}"
            },
            {
              "name": "endDate",
              "value": "={{ $('Globals').item.json.fullEndDate }}"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "7ba51941-90c8-419d-8bf7-fe16850e211f",
      "name": "Globals",
      "type": "n8n-nodes-base.set",
      "position": [
        -2288,
        656
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "3d75f148-28b5-40c2-be14-be432060c4ad",
              "name": "tmAccountId",
              "type": "string",
              "value": ""
            },
            {
              "id": "576633ab-0cf1-4e3f-b839-ac77acfa0cd3",
              "name": "Allow hours missing percentage",
              "type": "number",
              "value": 10
            },
            {
              "id": "1f92de22-2252-49d2-8960-fc259844d228",
              "name": "tmetricToSlackUserDataTableName",
              "type": "string",
              "value": "Tmetric to Slack user map"
            },
            {
              "id": "889bf3ce-a139-499b-8744-c71e498b3cc3",
              "name": "apiBaseUrl",
              "type": "string",
              "value": "https://app.tmetric.com/api/v3"
            },
            {
              "id": "04388835-8fd6-426a-9c09-8a065f224a2c",
              "name": "fullEndDate",
              "type": "string",
              "value": "={{ $now.format('yyyy-LL-dd') }}"
            },
            {
              "id": "fce6bbbf-4edc-42be-903c-b54f5523bdc8",
              "name": "beginningOfTheMonth",
              "type": "string",
              "value": "={{\n    new Date()\n      .toDateTime()\n      .set({\n        month: $now.month,\n        year: $now.year,\n        day: 1,\n      })\n      .format(\"yyyy-LL-dd\");\n  }}"
            },
            {
              "id": "b2747f37-4b7d-4a05-8ac5-e7cd60e17b6d",
              "name": "endOfTheMonth",
              "type": "string",
              "value": "={{\n    new Date()\n      .toDateTime()\n      .set({\n        month: $now.month + 1,\n        year: $now.year,\n        day: 0,\n      })\n      .format(\"yyyy-LL-dd\");\n  }}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "d2ba9f61-45aa-4c75-8bf9-ed633257d2df",
      "name": "Get TMetric Time Off Requests",
      "type": "n8n-nodes-base.httpRequest",
      "maxTries": 5,
      "position": [
        -1696,
        816
      ],
      "parameters": {
        "url": "={{$('Globals').item.json.apiBaseUrl}}/accounts/{{ $('Globals').item.json[\"tmAccountId\"] }}/timeoff/requests",
        "options": {},
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "startDate",
              "value": "={{ $('Globals').item.json.beginningOfTheMonth }}"
            },
            {
              "name": "endDate",
              "value": "={{ $('Globals').item.json.endOfTheMonth }}"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "304df868-05a3-4fb5-b744-b1a7d34801c4",
      "name": "Edit Fields",
      "type": "n8n-nodes-base.set",
      "position": [
        -1440,
        816
      ],
      "parameters": {
        "options": {
          "dotNotation": false
        },
        "assignments": {
          "assignments": [
            {
              "id": "63f0bad7-d96f-4fb2-824e-0a69454759eb",
              "name": "requestId",
              "type": "number",
              "value": "={{ $json.id }}"
            },
            {
              "id": "f29d8faa-c308-4b73-8cbe-9d0465d6f862",
              "name": "requesterId",
              "type": "number",
              "value": "={{ $json.requester.id }}"
            },
            {
              "id": "f5412568-7a70-4e84-b1b9-485df4d310f7",
              "name": "startDate",
              "type": "string",
              "value": "={{ $json.startDate }}"
            },
            {
              "id": "6eb42aa4-e4c6-458d-8ac2-7af1f5ab6bd3",
              "name": "endDate",
              "type": "string",
              "value": "={{ $json.endDate }}"
            },
            {
              "id": "d0755bc5-3105-4aed-9b85-600969c8e3ba",
              "name": "approved",
              "type": "boolean",
              "value": "={{ $json.status === 'Approved' }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "3327f549-bd31-4e67-b584-7129cc467321",
      "name": "Get Tmetric Time Entries per user",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        16,
        640
      ],
      "parameters": {
        "url": "={{$('Globals').item.json.apiBaseUrl}}/accounts/{{$('Globals').item.json.tmAccountId}}/timeentries",
        "options": {},
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "userId",
              "value": "={{ $json.user.id }}"
            },
            {
              "name": "startDate",
              "value": "={{$('Globals').item.json.beginningOfTheMonth}}"
            },
            {
              "name": "endDate",
              "value": "={{$('Globals').item.json.fullEndDate}}"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3,
      "alwaysOutputData": true
    },
    {
      "id": "046b0bbd-1dc4-48f6-a6e8-667d3fd37fe1",
      "name": "Wait",
      "type": "n8n-nodes-base.wait",
      "position": [
        1152,
        816
      ],
      "parameters": {},
      "typeVersion": 1.1
    },
    {
      "id": "b4f17fc5-d4c6-4176-b89c-79336b62653e",
      "name": "Filter out running tasks",
      "type": "n8n-nodes-base.filter",
      "position": [
        320,
        640
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "5ea8fc2b-a93b-473f-8476-9d903ec5a24b",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json?.endTime }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3,
      "alwaysOutputData": true
    },
    {
      "id": "42920371-649f-4ec8-81c0-ace49ff8be73",
      "name": "Rename Keys",
      "type": "n8n-nodes-base.renameKeys",
      "position": [
        -1440,
        640
      ],
      "parameters": {
        "keys": {
          "key": [
            {
              "newKey": "workSchedule",
              "currentKey": "days"
            }
          ]
        },
        "additionalOptions": {}
      },
      "typeVersion": 1
    },
    {
      "id": "8a970255-ae6d-43d8-8630-18675550cf07",
      "name": "Group Time off requests",
      "type": "n8n-nodes-base.set",
      "position": [
        -1216,
        816
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "cf496be6-8686-47ba-b9d6-b4b0f4342e80",
              "name": "=timeOffRequests",
              "type": "array",
              "value": "={{ $input.all().map(({json})=>json).filter(item=>item.requesterId == $json.requesterId) }}"
            },
            {
              "id": "e1b2ae83-66f8-4f1c-b04d-d9ba8f335a84",
              "name": "user",
              "type": "object",
              "value": "={\nid: {{ $json.requesterId }}\n}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "a127edc8-f6cc-47c2-b929-41ef76335354",
      "name": "Remove Duplicates",
      "type": "n8n-nodes-base.removeDuplicates",
      "position": [
        -992,
        816
      ],
      "parameters": {
        "compare": "selectedFields",
        "options": {},
        "fieldsToCompare": "user.id"
      },
      "typeVersion": 2
    },
    {
      "id": "1a0d9f70-28e4-40f6-b1e4-a238b3cb9101",
      "name": "Add User Work Schedule",
      "type": "n8n-nodes-base.merge",
      "position": [
        -1216,
        480
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "advanced": true,
        "joinMode": "enrichInput1",
        "mergeByFields": {
          "values": [
            {
              "field1": "id",
              "field2": "user.id"
            }
          ]
        }
      },
      "typeVersion": 3.2
    },
    {
      "id": "62e0f9ea-aa6e-42b6-abbc-68694744b10d",
      "name": "Add Time off requests",
      "type": "n8n-nodes-base.merge",
      "position": [
        -752,
        496
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "advanced": true,
        "joinMode": "enrichInput1",
        "mergeByFields": {
          "values": [
            {
              "field1": "user.id",
              "field2": "user.id"
            }
          ]
        }
      },
      "typeVersion": 3.2
    },
    {
      "id": "37b225ba-7484-464c-9423-80c51c08194c",
      "name": "User Repo",
      "type": "n8n-nodes-base.set",
      "position": [
        -512,
        496
      ],
      "parameters": {
        "mode": "raw",
        "options": {},
        "jsonOutput": "={\n  \"user\": {{ $json.user }},\n  \"workSchedule\": {{ $if($json.workSchedule.isNotEmpty(), $json.workSchedule, []) }},\n  \"timeOffRequests\": {{ $if($json.timeOffRequests.isNotEmpty(), $json.timeOffRequests, []) }}\n}\n"
      },
      "typeVersion": 3.4
    },
    {
      "id": "8cd20b81-637f-4533-a553-cdc6422e92f8",
      "name": "Create a data table",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        -2640,
        1728
      ],
      "parameters": {
        "columns": {
          "column": [
            {
              "name": "tmetric_user_id",
              "type": "number"
            },
            {
              "name": "slack_user_id"
            },
            {
              "name": "slack_display_name"
            },
            {
              "name": "slack_name"
            }
          ]
        },
        "options": {},
        "resource": "table",
        "operation": "create",
        "tableName": "={{ $('Globals').first().json.tmetricToSlackUserDataTableName }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "a272b693-d01d-4b19-be65-b1e168dbf3e2",
      "name": "Get many users",
      "type": "n8n-nodes-base.slack",
      "position": [
        -2160,
        1728
      ],
      "parameters": {
        "resource": "user",
        "operation": "getAll",
        "returnAll": true,
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.4
    },
    {
      "id": "6e4a81da-98e7-435f-ae92-99b2ef0f0e6d",
      "name": "Filter out users",
      "type": "n8n-nodes-base.filter",
      "position": [
        -1920,
        1728
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "5da844c8-0f2b-4e7d-a60e-da752c818c0e",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              },
              "leftValue": "={{ $json.is_bot }}",
              "rightValue": false
            },
            {
              "id": "a80edc73-60a2-4657-a338-4f873719ac17",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.id }}",
              "rightValue": "USLACKBOT"
            },
            {
              "id": "59cc112d-fab5-48c9-83e4-0f1269103b2a",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              },
              "leftValue": "={{ $json.deleted }}",
              "rightValue": false
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "8042d352-009a-4f12-b530-ed7504bb9671",
      "name": "Send message and wait for response",
      "type": "n8n-nodes-base.slack",
      "position": [
        -2272,
        2016
      ],
      "parameters": {
        "select": "channel",
        "message": "=Message for: <@{{ $json.id }}> ({{ $json.profile.display_name }})\nFind and select your TMetric account from the list\nIf not applicable, don't select anythging and press \"Respond\"",
        "options": {},
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "channel-id",
          "cachedResultName": "channel-to-sent-notifications-to"
        },
        "operation": "sendAndWait",
        "defineForm": "json",
        "jsonOutput": "=[\n{{\n    JSON.stringify({\n      fieldLabel: `TMetric account`,\n      fieldType: \"dropdown\",\n      fieldOptions: {\n        values: $(\"Get TMetric Users for user mapping\").item.json.users.map(\n          (user) => ({\n            option: `${user.name} <<${user.id}>>`,\n          }),\n        ),\n      },\n    })\n}}\n]",
        "responseType": "customForm",
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "executeOnce": false,
      "typeVersion": 2.4
    },
    {
      "id": "d148f6e0-f1a0-40b8-9b72-b8f6f0e73f31",
      "name": "Get TMetric Users for user mapping",
      "type": "n8n-nodes-base.httpRequest",
      "maxTries": 5,
      "position": [
        -2400,
        1728
      ],
      "parameters": {
        "url": "={{$('Globals').item.json.apiBaseUrl}}/accounts/{{ $('Globals').item.json[\"tmAccountId\"] }}/reports/projects/filter",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "fdd9e332-0afe-43e2-951b-f976f2f5e693",
      "name": "Setup",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -2512,
        1520
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "f1f06c41-d192-448a-b96e-e35b477a637f",
      "name": "Upsert row(s)",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        -2032,
        2016
      ],
      "parameters": {
        "columns": {
          "value": {
            "slack_user_id": "={{ $('Send form for each user').item.json.id }}",
            "tmetric_user_id": "={{ $json.data[\"TMetric account\"].match(/<<(\\d+)>>/gm)[0].replaceAll('<','').replaceAll('>','') }}",
            "slack_display_name": "={{ $('Send form for each user').item.json.profile.display_name }}"
          },
          "schema": [
            {
              "id": "tmetric_user_id",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "tmetric_user_id",
              "defaultMatch": false
            },
            {
              "id": "slack_user_id",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "slack_user_id",
              "defaultMatch": false
            },
            {
              "id": "slack_display_name",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "slack_display_name",
              "defaultMatch": false
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "filters": {
          "conditions": [
            {
              "keyName": "=slack_user_id",
              "keyValue": "={{ $('Send form for each user').item.json.id }}"
            }
          ]
        },
        "options": {},
        "operation": "upsert",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Globals').first().json.tmetricToSlackUserDataTableName }}"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "c7fd6f39-9682-436c-94e8-be2d0c8c65b6",
      "name": "List data tables",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        -2272,
        1520
      ],
      "parameters": {
        "options": {},
        "resource": "table"
      },
      "typeVersion": 1.1,
      "alwaysOutputData": true
    },
    {
      "id": "88c0036c-c469-4ba0-b469-b052ceb127ec",
      "name": "Does Data table already exist?",
      "type": "n8n-nodes-base.if",
      "position": [
        -2032,
        1520
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "383376f6-4f47-4921-b1c9-9427151394b3",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.name }}",
              "rightValue": "={{ $('Globals').first().json.tmetricToSlackUserDataTableName }}"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "17552eb1-9842-4d0d-80b9-0cc3988256c7",
      "name": "Send form for each user",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -2512,
        1920
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "802dc733-ab07-4e4b-94df-8d4171186d83",
      "name": "Merge user info with time entries",
      "type": "n8n-nodes-base.merge",
      "position": [
        928,
        528
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3.2
    },
    {
      "id": "59366b0e-24ab-49b3-8e94-49b65d517f45",
      "name": "Extract data from Time Entries",
      "type": "n8n-nodes-base.set",
      "position": [
        640,
        640
      ],
      "parameters": {
        "mode": "raw",
        "options": {},
        "jsonOutput": "={\n  \"time_entries\": {{ $input.all().map(({json})=>({\n    \"project\": json?.project,\n    \"task\": $if(json?.task, json?.task.name, json.note),\n    \"startTime\": json.startTime,\n    \"endTime\": json.endTime,\n    \"timeSpentInSeconds\": json?.endTime?.toDateTime().diffTo(json?.startTime, 'seconds'),\n    \"note\": json?.note,\n    \"isBillable\": json?.isBillable,\n    \"isInvoiced\": json?.isInvoiced\n  })).filter(obj=>obj.project) }}\n}\n"
      },
      "executeOnce": true,
      "typeVersion": 3.4
    },
    {
      "id": "57e08fd3-52fe-4b92-863d-ac0732c19760",
      "name": "Is running Setup?",
      "type": "n8n-nodes-base.if",
      "position": [
        -2048,
        656
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "228608a5-9f31-48e9-ac60-149105dc5d70",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json[\"Day of week\"] }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "be85e888-d502-41f2-88fb-c555ae629dfa",
      "name": "Send a message",
      "type": "n8n-nodes-base.slack",
      "position": [
        640,
        96
      ],
      "parameters": {
        "user": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.slack_user_id }}"
        },
        "select": "user",
        "blocksUi": "={\n\t\"blocks\": [\n\t\t{\n\t\t\t\"type\": \"header\",\n\t\t\t\"text\": {\n\t\t\t\t\"type\": \"plain_text\",\n\t\t\t\t\"text\": \"Hi {{$json.slack_display_name}}\",\n\t\t\t\t\"emoji\": true\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"type\": \"section\",\n\t\t\t\"text\": {\n\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\"text\": \"Here is your weekly report:\"\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"type\": \"divider\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"actions\",\n\t\t\t\"elements\": [\n                {{ $if($json.computed.percentageBehindSchedule > $('Globals').first().json['Allow hours missing percentage'], `\n{\n\t\t\t\t\t\"type\": \"button\",\n\t\t\t\t\t\"text\": {\n\t\t\t\t\t\t\"type\": \"plain_text\",\n\t\t\t\t\t\t\"emoji\": true,\n\t\t\t\t\t\t\"text\": \"You are ${$json.computed.timeBehindScheduleHours}:${$json.computed.timeBehindScheduleMinutes}h behind the schedule :(\"\n\t\t\t\t\t},\n\t\t\t\t\t\"style\": \"danger\",\n\t\t\t\t\t\"value\": \"click_me_123\"\n\t\t\t\t}\n`, `\n{\n\t\t\t\t\t\"type\": \"button\",\n\t\t\t\t\t\"text\": {\n\t\t\t\t\t\t\"type\": \"plain_text\",\n\t\t\t\t\t\t\"emoji\": true,\n\t\t\t\t\t\t\"text\": \"Good work, everything adds up :)\"\n\t\t\t\t\t},\n\t\t\t\t\t\"style\": \"primary\",\n\t\t\t\t\t\"value\": \"click_me_123\"\n\t\t\t\t}\n`) }}  \n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"type\": \"divider\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"header\",\n\t\t\t\"text\": {\n\t\t\t\t\"type\": \"plain_text\",\n\t\t\t\t\"text\": \"Projects summary:\",\n\t\t\t\t\"emoji\": true\n\t\t\t}\n\t\t},{{ $json.computed.projects?.map(project=>`\n{\n\t\t\t\"type\": \"section\",\n\t\t\t\"fields\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Name:*\\\\n${project.project.name}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Client:*\\\\n${project.project.client.name}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Total time spent:*\\\\n${Math.floor(project.project.timeSpentInSeconds / 60 / 60)}:${Math.floor(project.project.timeSpentInSeconds / 60 % 60)}h\"\n\t\t\t\t}\n\t\t\t]\n\t\t},`).join('') }}\n{\n\t\t\t\"type\": \"divider\"\n\t\t}\n\t]\n}",
        "messageType": "block",
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.4
    },
    {
      "id": "93ba4d70-3586-462a-a841-fdf5e735f55e",
      "name": "Get row(s)",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        16,
        80
      ],
      "parameters": {
        "operation": "get",
        "returnAll": true,
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Globals').first().json.tmetricToSlackUserDataTableName }}"
        }
      },
      "executeOnce": true,
      "typeVersion": 1.1
    },
    {
      "id": "4b3867e7-14b3-4033-8cc8-a38e355ae531",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        320,
        96
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "advanced": true,
        "joinMode": "enrichInput2",
        "mergeByFields": {
          "values": [
            {
              "field1": "tmetric_user_id",
              "field2": "user.id"
            }
          ]
        }
      },
      "typeVersion": 3.2
    },
    {
      "id": "331cc624-6e7e-45cc-bafc-b22a4caa8ae1",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1856,
        176
      ],
      "parameters": {
        "color": 7,
        "width": 1472,
        "height": 864,
        "content": "## Step 1:  Collecting the data from Tmetric and process it\nIn this step we use custom API calls to Tmetric API to collect are necessary data. Then we match it to the correct user, assign it to make easier to use later.\n\nFor Work Schedule we get data from beginning of the month up until now, so we can calculate missing time correctly\nFor Projects Data we fetch entire month so we know how much of the budget was spent\n\nIn the end we use set to create cohesieve repository that will be easy to work with\n\nIf you want to learn more about Tmetric API check their docs [here](https://app.tmetric.com/api-docs/)"
      },
      "typeVersion": 1
    },
    {
      "id": "328aef0e-03cc-46e3-95ba-0dc04d2d035d",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2592,
        176
      ],
      "parameters": {
        "color": 5,
        "width": 720,
        "height": 864,
        "content": "## Set up Globals and check if workflow runs from schedule or click\nBecause we specify in \"Globals\" data that is required also in \"Setup\" we must check what invoked \"If\" node. Luckily, it's very easy, because \"Run Once a Week\" trigger returns data which we can check against in our \"If\" node"
      },
      "typeVersion": 1
    },
    {
      "id": "4b7a5251-112d-4ae2-8e65-00f8cadccd32",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -368,
        288
      ],
      "parameters": {
        "color": 7,
        "width": 1952,
        "height": 752,
        "content": "## Step 2: Process each user, fetch more user specific data, and generate data for personalized feedback\nIn this step we have to go back to Tmetric API. Using previously obtained User ID, we are obtaining users Time Entries without currently running tasks that are later used to calculate how much time they in fact spent working. \nWe also use Work Schedule and Time Off Requests to see how much time user should actually have spent. Then, we convert it to percentages to see if user met our criteria\n\nWe use wait node to take a short break between successive calls so we don't hit rate limits."
      },
      "typeVersion": 1
    },
    {
      "id": "42d881fb-8cbd-4de1-8698-4ff03f5744df",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -368,
        -144
      ],
      "parameters": {
        "color": 7,
        "width": 1952,
        "height": 416,
        "content": "## Step 3: Send raport\nIn this step we use N8N Data Table we created previously and look for users Slack ID so we can send them message directly.\n\nMessage is built using Slack Block Builder Kit available under [this link](https://app.slack.com/block-kit-builder/). With it's help we can create styled messages that are not boring. "
      },
      "typeVersion": 1
    },
    {
      "id": "42a65dee-35c6-4e2b-9553-d55b583439ae",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2704,
        1056
      ],
      "parameters": {
        "color": 3,
        "width": 960,
        "height": 1168,
        "content": "## Run once before you run on schedule\n\nThis workflow requires N8N Data Table in order to work. Once you run \"Setup\" workflow will check if required Data Table already exists, if no, it will create it.\nThen, it will make API Call to Tmetric, retrieve users data, retrieve Slack users and filter out accounts that are invalid.\n\nThen it will send a message to the channel, pinging specific users to fill out the form where they are requested to select their Tmetric account.\n\nThis is needed in order to be able to send later personalized private messages directly to the users\n\n### Run again\n\nIf new user has been added to Tmetric or Slack, or somebody made mistake, you can safely run this step again. It won't recreate the table but send a form again to all users\n\n### Important\n\nIf user shouldn't be notified they should just submit the form without selecting any value"
      },
      "typeVersion": 1
    },
    {
      "id": "1d4a1351-f469-4dcf-8815-918c9046018f",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3472,
        -96
      ],
      "parameters": {
        "width": 864,
        "height": 1136,
        "content": "## TMetric to Slack employees time manager\n### How it works\nThis workflow checks weekly if users meet their hourly quota at the end of trhe week.\nIt will help them keep track of how much they worked, on what projects, remind them if they forgot to put entries in the system\n\nIt leverages Tmetric API in order to obtain data, Slack, and N8N Data Tables\n\n#### Create tables\n1. We check if data table with given name already exists, if yes, we skip to the next part, if not, we create it\n2. We get Tmetric and Slack users, filter out users that are bots or inactive\n3. We sen\n\n#### Run Once a Week\n1. We colect data from TMetric using custom HTTP Requests, calculate percentage of project budget spent, getting users, their Work Schedules and Time Off Requests\n2. We process each user individually, get their Time Entries, merge all data together into one cohesieve piece\n3. We get users Slack id from N8N Data table we created during set up using their Tmetric id to send personalized message directly to them\n\n###Setup\nIn order for workflow to work you will need to setup couple variables. Most likely you will be only interested in just 2 first, but you can also set name of the Data Table to prevent any problems\n\n- Fill out \"Globals\" node\n  - tmAccountId - You can find it in URL once you visit Tmetric, in this example it's all 0's: https://app.tmetric.com/#/tracker/000000/\n  - Allow hours missing percentage - If users have more percentages of missing time spent on working they will get information about it\n  - tmetricToSlackUserDataTableName - Name of the data table used across the project \n\n- Auth\nIn order for this workflow to work you have to set up auth for HTTP Request Nodes. In order to do this, open one of these nodes and select:\nAuthentication > General Credential Type > Header Auth \nAnd if you doesn't already have Tmetric credentials Create new of following shape:\n\nName:\nAuthorization\nValue:\n<Your Tmetric API Key>\n\nYou can learn how to get API key [here](https://app.tmetric.com/api-docs/)\n\n### Customize\nIf you want to customize message that is sent on Slack edit \"Send a message\" node. You can also change there if message should be sent directly or to the channel\n\n\n\n\n\nNeed help? Contact us at developers@sailingbyte.com or at sailingbyte.com\n\nHappy hacking!"
      },
      "typeVersion": 1
    },
    {
      "id": "1e1ab902-8d49-4073-9a9f-2523927be8e8",
      "name": "Calculate data for personalized message",
      "type": "n8n-nodes-base.code",
      "position": [
        1408,
        816
      ],
      "parameters": {
        "jsCode": "const projectsMeta = $('Percentages of budget spent').all().map(p=>p.json)\n\nconsole.log(projectsMeta)\n\nfor (const item of $input.all()) {\n  item.json.computed = {};\n  item.json.computed.timeWorkedInSeconds = item.json.time_entries.reduce(\n    (acc, curr) => {\n      console.log(curr);\n      return acc + curr.timeSpentInSeconds;\n    },\n    0,\n  );\n  const normalizedTimeOffRequests = item.json.timeOffRequests\n    .map((req) => ({\n      ...req,\n      startDate: req.startDate.slice(0, 10),\n      endDate: req.endDate.slice(0, 10),\n    }))\n    .map(({ startDate, endDate, ...req }) => [\n      { ...req, date: startDate },\n      { ...req, date: endDate },\n    ])\n    .flat();\n  console.log(normalizedTimeOffRequests);\n  item.json.computed.workScheduleInSecondsWithoutTimeOffRequests =\n    item.json.workSchedule\n      .filter((workScheduleItem) => workScheduleItem.isWorking)\n      .reduce((acc, curr) => {\n        if (normalizedTimeOffRequests.includes((req) => req.date === curr.date))\n          return 0;\n        return acc + (curr?.hours ?? 0);\n      }, 0) *\n    60 *\n    60;\n  item.json.computed.timeBehindScheduleInSeconds =\n    item.json.computed.workScheduleInSecondsWithoutTimeOffRequests -\n    item.json.computed.timeWorkedInSeconds;\n  item.json.computed.timeBehindScheduleHours = Math.floor(\n    Math.abs(item.json.computed.timeBehindScheduleInSeconds) / 3600,\n  );\n  item.json.computed.timeBehindScheduleMinutes = Math.floor(\n    (Math.abs(item.json.computed.timeBehindScheduleInSeconds) % 3600) / 60,\n  );\n  item.json.computed.percentageBehindSchedule =\n    (item.json.computed.timeBehindScheduleInSeconds /\n      item.json.computed.workScheduleInSecondsWithoutTimeOffRequests) *\n    100;\n  const getSeconds = (start, end) => (new Date(end) - new Date(start)) / 1000;\n  const projectMap = {};\n\n  item.json.time_entries.forEach((entry) => {\n    const projectId = entry.project.id;\n\n    if (!projectMap[projectId]) {\n      projectMap[projectId] = {\n        ...entry.project,\n        timeSpentInSeconds: 0,\n        timeEntries: {},\n      };\n    }\n\n    const project = projectMap[projectId];\n    const duration = getSeconds(entry.startTime, entry.endTime);\n\n    const taskKey = entry.task ? `task-${entry.task.id}` : `note-${entry.note}`;\n\n    if (!project.timeEntries[taskKey]) {\n      project.timeEntries[taskKey] = {\n        note: entry.note,\n        ...(entry.task && { task: entry.task }),\n        tags: entry.tags,\n        isBillable: entry.isBillable,\n        isInvoiced: entry.isInvoiced,\n        startTime: entry.startTime,\n        endTime: entry.endTime,\n        timeSpentInSeconds: 0,\n      };\n    }\n\n    project.timeSpentInSeconds += duration;\n    project.timeEntries[taskKey].timeSpentInSeconds += duration;\n\n    if (\n      new Date(entry.endTime) > new Date(project.timeEntries[taskKey].endTime)\n    ) {\n      project.timeEntries[taskKey].endTime = entry.endTime;\n    }\n  });\n\n  const result = Object.values(projectMap).map((project) => {\n    return {\n      project: {\n        ...project,\n        budgetSpentPercentages: projectsMeta.find(p=>p.id === project.id)?.budget.spentPercentages ?? '[???]',\n        timeEntries: Object.values(project.timeEntries),\n      },\n    };\n  });\n  item.json.computed.projects = result;\n}\n\nreturn $input.all();\n"
      },
      "executeOnce": true,
      "typeVersion": 2
    }
  ],
  "connections": {
    "Wait": {
      "main": [
        [
          {
            "node": "Calculate data for personalized message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Send a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Setup": {
      "main": [
        [
          {
            "node": "Globals",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Globals": {
      "main": [
        [
          {
            "node": "Is running Setup?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "User Repo": {
      "main": [
        [
          {
            "node": "Process Each User Separately",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get row(s)": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields": {
      "main": [
        [
          {
            "node": "Group Time off requests",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rename Keys": {
      "main": [
        [
          {
            "node": "Add User Work Schedule",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Upsert row(s)": {
      "main": [
        [
          {
            "node": "Send form for each user",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get many users": {
      "main": [
        [
          {
            "node": "Filter out users",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run Once a Week": {
      "main": [
        [
          {
            "node": "Globals",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter out users": {
      "main": [
        [
          {
            "node": "Send form for each user",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "List data tables": {
      "main": [
        [
          {
            "node": "Does Data table already exist?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Users Data": {
      "main": [
        [
          {
            "node": "Add User Work Schedule",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get TMetric Users": {
      "main": [
        [
          {
            "node": "Split Users Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is running Setup?": {
      "main": [
        [
          {
            "node": "Get TMetric Users",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get TMetric User Work Schedule",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get TMetric Time Off Requests",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "List data tables",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Remove Duplicates": {
      "main": [
        [
          {
            "node": "Add Time off requests",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Create a data table": {
      "main": [
        [
          {
            "node": "Get TMetric Users for user mapping",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add Time off requests": {
      "main": [
        [
          {
            "node": "User Repo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add User Work Schedule": {
      "main": [
        [
          {
            "node": "Add Time off requests",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Group Time off requests": {
      "main": [
        [
          {
            "node": "Remove Duplicates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send form for each user": {
      "main": [
        [],
        [
          {
            "node": "Send message and wait for response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter out running tasks": {
      "main": [
        [
          {
            "node": "Extract data from Time Entries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process Each User Separately": {
      "main": [
        [
          {
            "node": "Get row(s)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ],
        [
          {
            "node": "Get Tmetric Time Entries per user",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge user info with time entries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get TMetric Time Off Requests": {
      "main": [
        [
          {
            "node": "Edit Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Does Data table already exist?": {
      "main": [
        [
          {
            "node": "Get TMetric Users for user mapping",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Create a data table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract data from Time Entries": {
      "main": [
        [
          {
            "node": "Merge user info with time entries",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Get TMetric User Work Schedule": {
      "main": [
        [
          {
            "node": "Rename Keys",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Tmetric Time Entries per user": {
      "main": [
        [
          {
            "node": "Filter out running tasks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge user info with time entries": {
      "main": [
        [
          {
            "node": "Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get TMetric Users for user mapping": {
      "main": [
        [
          {
            "node": "Get many users",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send message and wait for response": {
      "main": [
        [
          {
            "node": "Upsert row(s)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate data for personalized message": {
      "main": [
        [
          {
            "node": "Process Each User Separately",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}