{
  "nodes": [
    {
      "id": "fc46a11c-cb88-470d-ac28-314a4f911676",
      "name": "Send an Email",
      "type": "n8n-nodes-base.emailSend",
      "position": [
        2704,
        176
      ],
      "parameters": {
        "html": "={{ $json.html }}",
        "options": {
          "appendAttribution": false,
          "allowUnauthorizedCerts": true
        },
        "subject": "={{ $json.subject }}",
        "toEmail": "info@example.com",
        "fromEmail": "info@example.com"
      },
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "51e18cb1-20e1-486a-9ea5-a9c65bb72a27",
      "name": "Every Day at 9:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        736,
        256
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 9 * * 1-5"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "f228e077-eae6-4442-abd5-46d713a980ff",
      "name": "GET Projects",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        960,
        256
      ],
      "parameters": {
        "url": "https://kimai/api/projects",
        "options": {},
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "visible",
              "value": "1"
            }
          ]
        }
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "055cc8ac-f30a-4f69-a236-edd0425b58cc",
      "name": "Get only Bilable",
      "type": "n8n-nodes-base.code",
      "position": [
        1184,
        256
      ],
      "parameters": {
        "jsCode": "const results = [];\n\nfor (const item of $input.all()) {\n  if (item.json.billable === true) {\n    results.push({ json: item.json });\n  }\n}\n\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "0e709eaf-f296-4e60-8fa5-20739dd4a9e8",
      "name": "GET Projects Details",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1552,
        160
      ],
      "parameters": {
        "url": "=https://kimai/api/projects/{{$json.id}}",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "0a14fabc-e525-4c78-8291-b1f9dad92492",
      "name": "GET Timesheet Records",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1408,
        352
      ],
      "parameters": {
        "url": "https://kimai/api/timesheets",
        "options": {},
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "user",
              "value": "all"
            },
            {
              "name": "project",
              "value": "={{ $json.id }}"
            },
            {
              "name": "size",
              "value": "1+1234567890"
            }
          ]
        }
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "3453b9a8-0e24-422b-972f-ad490bdc8fcf",
      "name": "Calculate Budget Uses",
      "type": "n8n-nodes-base.code",
      "position": [
        1632,
        352
      ],
      "parameters": {
        "jsCode": "const totali = {};\n\nfor (const item of $input.all()) {\n  const projectId = item.json.project?.id ?? item.json.project ?? null;\n  const duration = Number(item.json.duration ?? 0);\n\n  if (!projectId) continue;\n\n  if (!totali[projectId]) {\n    totali[projectId] = 0;\n  }\n\n  totali[projectId] += duration;\n}\n\nconst results = Object.entries(totali)\n  .map(([id, totalSeconds]) => ({\n    json: {\n      id: Number(id),\n      total_seconds: totalSeconds,\n      total_ore: Math.round((totalSeconds / 3600) * 100) / 100\n    }\n  }))\n  .sort((a, b) => a.json.id - b.json.id);\n\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "381fbd24-3e48-4d3f-8b92-ae73df6d8456",
      "name": "Combine Data",
      "type": "n8n-nodes-base.merge",
      "position": [
        1856,
        256
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "joinMode": "enrichInput1",
        "fieldsToMatchString": "id"
      },
      "typeVersion": 3.2
    },
    {
      "id": "1f697c3b-9d42-4f15-ac8e-98c5b0490a38",
      "name": "Calculate expiration",
      "type": "n8n-nodes-base.code",
      "position": [
        2080,
        256
      ],
      "parameters": {
        "jsCode": "const daysThreshold = 10;\n\nconst now = new Date();\nconst today = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n\nconst msPerDay = 1000 * 60 * 60 * 24;\nconst projects = [];\n\nfunction formatIsoToItalian(isoDate) {\n  if (!isoDate) return null;\n\n  const parts = String(isoDate).split(\"-\");\n  if (parts.length !== 3) return null;\n\n  return `${parts[2]}/${parts[1]}/${parts[0]}`;\n}\n\nfunction getDaysDiff(date) {\n  if (!date) return null;\n  return Math.round((date.getTime() - today.getTime()) / msPerDay);\n}\n\nfunction getUrgency(days) {\n  if (days === null || days === undefined) return \"none\";\n  if (days < 0) return \"expired\";\n  if (days <= 3) return \"high\";\n  if (days <= 7) return \"medium\";\n  return \"low\";\n}\n\nfunction getStatus(days) {\n  if (days === null || days === undefined) return \"not_present\";\n  return days < 0 ? \"expired\" : \"expiring\";\n}\n\nfunction getBudgetInfo(timeBudget, totalSeconds) {\n  if (!timeBudget || timeBudget <= 0) return null;\n\n  const percentage = Math.round((totalSeconds / timeBudget) * 100);\n  const usedHours = Math.round((totalSeconds / 3600) * 100) / 100;\n  const budgetHours = Math.round((timeBudget / 3600) * 100) / 100;\n  const remainingHours = Math.round(((timeBudget - totalSeconds) / 3600) * 100) / 100;\n\n  return {\n    percentage,\n    usedHours,\n    budgetHours,\n    remainingHours,\n    exceeded: percentage >= 100,\n    shouldReport: percentage >= 80\n  };\n}\n\nfor (const item of $input.all()) {\n  const id = item.json.id ?? null;\n  const name = String(item.json.name ?? \"\");\n  const customer = String(item.json.parentTitle ?? \"\");\n\n  const endRaw = item.json.end ?? null;\n\n  let projectDeadline = \"not present\";\n  let projectDeadlineIso = null;\n  let projectDays = null;\n  let projectUrgency = \"none\";\n  let projectStatus = \"not_present\";\n\n  if (endRaw) {\n    const endDate = new Date(`${endRaw}T00:00:00`);\n    if (!isNaN(endDate.getTime())) {\n      projectDeadline = formatIsoToItalian(endRaw) || endRaw;\n      projectDeadlineIso = endRaw;\n      projectDays = getDaysDiff(endDate);\n      projectUrgency = getUrgency(projectDays);\n      projectStatus = getStatus(projectDays);\n    }\n  }\n\n  const timeBudget = Number(item.json.timeBudget ?? 0);\n  const totalSeconds = Number(item.json.total_seconds ?? 0);\n  const budget = getBudgetInfo(timeBudget, totalSeconds);\n\n  const projectShouldReport =\n    projectDays !== null && projectDays >= 0 && projectDays <= daysThreshold;\n\n  const budgetShouldReport =\n    budget !== null && budget.shouldReport;\n\n  const shouldReport =\n    projectShouldReport ||\n    budgetShouldReport;\n\n  if (!shouldReport) {\n    continue;\n  }\n\n  projects.push({\n    id,\n    name,\n    customer,\n\n    project_deadline: projectDeadline,\n    project_deadline_iso: projectDeadlineIso,\n    project_days: projectDays,\n    project_urgency: projectUrgency,\n    project_status: projectStatus,\n\n    budget_set: timeBudget > 0,\n    budget_total_hours: budget ? budget.budgetHours : null,\n    budget_used_hours: budget ? budget.usedHours : null,\n    budget_remaining_hours: budget ? budget.remainingHours : null,\n    budget_percentage: budget ? budget.percentage : null,\n    budget_exceeded: budget ? budget.exceeded : false,\n    budget_should_report: budget ? budget.shouldReport : false\n  });\n}\n\nprojects.sort((a, b) => {\n  const aVal = a.project_days ?? 999999;\n  const bVal = b.project_days ?? 999999;\n  return aVal - bVal;\n});\n\nreturn [\n  {\n    json: {\n      daysThreshold,\n      count: projects.length,\n      projects\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "753c9210-efd8-43bf-993c-6f50799b0dfb",
      "name": "Need Email?",
      "type": "n8n-nodes-base.if",
      "position": [
        2304,
        256
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "condition-count",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $json.count }}",
              "rightValue": 0
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "a5294383-503e-4c15-a5e7-51e6e3ec0ed8",
      "name": "Build Email HTML - Report",
      "type": "n8n-nodes-base.code",
      "position": [
        2528,
        176
      ],
      "parameters": {
        "jsCode": "const projects = $json.projects || [];\nconst todayDate = $now.setLocale('en').toFormat('dd LLL yyyy');\nconst count = Number($json.count || 0);\nconst daysThreshold = Number($json.daysThreshold || 10);\n\nconst urgencyConfig = {\n  expired: { color: '#ef4444', background: '#fef2f2', icon: '\ud83d\udea8', label: 'EXPIRED', action: 'TAKE ACTION' },\n  high:    { color: '#f97316', background: '#fff7ed', icon: '\ud83d\udd25', label: 'URGENT', action: 'DUE SOON' },\n  medium:  { color: '#d97706', background: '#fffbeb', icon: '\u23f3', label: 'WARNING', action: 'TO MONITOR' },\n  low:     { color: '#10b981', background: '#ecfdf5', icon: '\u2705', label: 'OK', action: 'ON TRACK' },\n  none:    { color: '#6366f1', background: '#f5f3ff', icon: '\ud83d\udd0d', label: 'TO CHECK', action: 'MISSING DATA' }\n};\n\nconst escapeHtml = (v) =>\n  String(v ?? '')\n    .replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;');\n\nconst buildStatusBlock = (urgency, date, label) => {\n  const cfg = urgencyConfig[urgency] || urgencyConfig.none;\n  const isMissing = !date || date === 'not present';\n\n  return `\n    <div style=\"background: ${cfg.background}; border-radius: 16px; padding: 14px; border: 1px solid ${cfg.color}${isMissing ? '60' : '20'}; min-height: 80px;\">\n      <div style=\"display: flex; align-items: center; margin-bottom: 6px;\">\n        <span style=\"font-size: 14px; margin-right: 6px;\">${cfg.icon}</span>\n        <span style=\"font-size: 10px; font-weight: 800; color: ${cfg.color}; text-transform: uppercase; letter-spacing: 1px;\">${cfg.label}</span>\n      </div>\n      <div style=\"font-size: 15px; font-weight: 800; color: ${isMissing ? cfg.color : '#1e293b'}; margin-bottom: 2px;\">\n        ${escapeHtml(isMissing ? 'MISSING DATA' : date)}\n      </div>\n      <div style=\"font-size: 11px; color: #64748b; font-weight: 500;\">${escapeHtml(label)}</div>\n    </div>\n  `;\n};\n\nconst buildBudgetBlock = (p) => {\n  if (!p.budget_set) {\n    return `\n      <div style=\"background: #f5f3ff; border-radius: 16px; padding: 16px; border: 1px dashed #6366f1; text-align: center;\">\n        <div style=\"font-size: 14px; margin-bottom: 4px;\">\ud83d\udd0d</div>\n        <div style=\"font-size: 11px; font-weight: 800; color: #6366f1; text-transform: uppercase;\">Budget not configured</div>\n        <div style=\"font-size: 10px; color: #64748b; margin-top: 2px;\">Check whether this project should be billable</div>\n      </div>`;\n  }\n\n  const percent = Math.min(Number(p.budget_percentage ?? 0), 100);\n  const isOver = p.budget_exceeded;\n  const color = isOver ? '#ef4444' : (percent > 85 ? '#f97316' : '#3b82f6');\n\n  return `\n    <div style=\"background: #ffffff; border-radius: 16px; padding: 16px; border: 1px solid #e2e8f0;\">\n      <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n        <tr>\n          <td style=\"font-size: 12px; font-weight: 700; color: #1e293b;\">Budget Usage</td>\n          <td align=\"right\" style=\"font-size: 12px; font-weight: 800; color: ${color};\">${p.budget_percentage}%</td>\n        </tr>\n      </table>\n      <div style=\"width: 100%; height: 8px; background: #f1f5f9; border-radius: 10px; margin: 10px 0; overflow: hidden;\">\n        <div style=\"width: ${percent}%; height: 100%; background: ${color}; border-radius: 10px;\"></div>\n      </div>\n      <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n        <tr>\n          <td style=\"font-size: 11px; color: #64748b;\"><strong>${p.budget_used_hours}h</strong> logged</td>\n          <td align=\"right\" style=\"font-size: 11px; color: #64748b;\">Total: <strong>${p.budget_total_hours}h</strong></td>\n        </tr>\n      </table>\n    </div>`;\n};\n\nconst rowsHTML = projects.map((p) => {\n  const projectLabel =\n    p.project_status === 'not_present'\n      ? 'Add the PO deadline'\n      : (p.project_days < 0\n          ? `Expired ${Math.abs(p.project_days)} days ago`\n          : `In ${p.project_days} days`);\n\n  return `\n    <div style=\"margin-bottom: 30px; background: #ffffff; border-radius: 24px; box-shadow: 0 10px 25px rgba(0,0,0,0.05); border: 1px solid #e2e8f0; overflow: hidden;\">\n      <div style=\"padding: 20px; background: linear-gradient(to right, #f8fafc, #ffffff); border-bottom: 1px solid #f1f5f9;\">\n        <div style=\"font-size: 10px; font-weight: 800; color: #3b82f6; text-transform: uppercase; letter-spacing: 1.5px; margin-bottom: 4px;\">${escapeHtml(p.customer)}</div>\n        <div style=\"font-size: 20px; font-weight: 900; color: #0f172a; letter-spacing: -0.5px;\">${escapeHtml(p.name)}</div>\n      </div>\n      \n      <div style=\"padding: 20px;\">\n        <div style=\"font-size: 10px; font-weight: 700; color: #94a3b8; text-transform: uppercase; margin-bottom: 8px;\">Order Deadline (PO)</div>\n        ${buildStatusBlock(p.project_urgency, p.project_deadline, projectLabel)}\n\n        <div style=\"height: 16px;\"></div>\n\n        <div style=\"font-size: 10px; font-weight: 700; color: #94a3b8; text-transform: uppercase; margin-bottom: 8px;\">Hours Monitoring</div>\n        ${buildBudgetBlock(p)}\n      </div>\n    </div>`;\n}).join('');\n\nconst html = `<!DOCTYPE html>\n<html lang=\"en\">\n<body style=\"margin:0; padding:0; background-color:#f8fafc; font-family:'Inter', -apple-system, system-ui, sans-serif;\">\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"background-color: #f8fafc; padding: 40px 10px;\">\n    <tr>\n      <td align=\"center\">\n        <table width=\"600\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"width: 100%; max-width: 600px;\">\n          <tr>\n            <td style=\"padding-bottom: 30px; text-align: center;\">\n              <div style=\"font-size: 12px; font-weight: 800; color: #3b82f6; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 8px;\">Timesheet Analysis</div>\n              <h1 style=\"font-size: 36px; font-weight: 900; color: #0f172a; margin: 0; letter-spacing: -1.5px;\">Deadline Report</h1>\n              <p style=\"font-size: 16px; color: #64748b; margin-top: 10px;\">Detected <strong>${count} projects</strong> to manage or verify.</p>\n            </td>\n          </tr>\n\n          <tr>\n            <td>${rowsHTML}</td>\n          </tr>\n\n          <tr>\n            <td style=\"padding-top: 20px; text-align: center;\">\n              <a href=\"https://kimai.com\" style=\"display: inline-block; background: #0f172a; color: #ffffff; padding: 16px 40px; border-radius: 18px; text-decoration: none; font-weight: 700; font-size: 15px; box-shadow: 0 10px 20px rgba(15,23,42,0.1);\">Open Timesheet &rarr;</a>\n              <div style=\"margin-top: 40px; border-top: 1px solid #e2e8f0; padding-top: 20px; font-size: 12px; color: #94a3b8; letter-spacing: 0.5px;\">\n                Automatically generated on ${todayDate}\n              </div>\n            </td>\n          </tr>\n        </table>\n      </td>\n    </tr>\n  </table>\n</body>\n</html>`;\n\nreturn [\n  {\n    json: {\n      html,\n      subject: `[Timesheet] - Projects Deadline Report`\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "d9e85095-fd10-4ad2-aa0e-15d2a44163d5",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2016,
        544
      ],
      "parameters": {
        "color": 3,
        "width": 224,
        "height": 80,
        "content": "To Customize the Range of Day to check customize here."
      },
      "typeVersion": 1
    },
    {
      "id": "e0c4fc70-0a02-47e4-985a-8f30eca5690c",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        640,
        32
      ],
      "parameters": {
        "color": 7,
        "width": 256,
        "height": 608,
        "content": "## 1. Scheduled Start"
      },
      "typeVersion": 1
    },
    {
      "id": "b7ef4e00-7322-4614-85aa-2d99ef0f5656",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        912,
        32
      ],
      "parameters": {
        "color": 7,
        "width": 1072,
        "height": 608,
        "content": "## 2.  Get Information from Kimai"
      },
      "typeVersion": 1
    },
    {
      "id": "e70f095a-c231-4a93-b813-ffd7d1acfb46",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2000,
        32
      ],
      "parameters": {
        "color": 7,
        "width": 256,
        "height": 608,
        "content": "## 3. Check Expiration"
      },
      "typeVersion": 1
    },
    {
      "id": "59990bba-f5c5-4b4a-8fb0-f845d04e3d8b",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2272,
        32
      ],
      "parameters": {
        "color": 7,
        "width": 624,
        "height": 608,
        "content": "## 4. Build & Send Email"
      },
      "typeVersion": 1
    },
    {
      "id": "6aae9924-997a-4887-a127-1a8a35a7fd15",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -144,
        32
      ],
      "parameters": {
        "width": 768,
        "height": 608,
        "content": "# \ud83d\udcc5 Kimai \u2014 Deadline & Budget Monitor\n\n## Monitors billable Kimai projects daily and sends an HTML alert email when a deadline is within 10 days or the hour budget exceeds 80%.\n\nRuns every weekday at **9 AM**.\nFetches all billable projects and checks:\n- End date within **10 days**\n- Budget consumption **\u2265 80%**\n\nNo alerts needed? No email sent.\n\n## \u2699\ufe0f Customize\n\n| What | Where |\n|---|---|\n| Days threshold | `Calculate expiration` \u2192 line 1 |\n| Budget % alert | `Calculate expiration` \u2192 `getBudgetInfo()` |\n| Kimai URL | All 3 HTTP Request nodes |\n| Sender / Recipient | `Send an Email` node |\n"
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Need Email?": {
      "main": [
        [
          {
            "node": "Build Email HTML - Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine Data": {
      "main": [
        [
          {
            "node": "Calculate expiration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GET Projects": {
      "main": [
        [
          {
            "node": "Get only Bilable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get only Bilable": {
      "main": [
        [
          {
            "node": "GET Projects Details",
            "type": "main",
            "index": 0
          },
          {
            "node": "GET Timesheet Records",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every Day at 9:00": {
      "main": [
        [
          {
            "node": "GET Projects",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate expiration": {
      "main": [
        [
          {
            "node": "Need Email?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GET Projects Details": {
      "main": [
        [
          {
            "node": "Combine Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Budget Uses": {
      "main": [
        [
          {
            "node": "Combine Data",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "GET Timesheet Records": {
      "main": [
        [
          {
            "node": "Calculate Budget Uses",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Email HTML - Report": {
      "main": [
        [
          {
            "node": "Send an Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}