{
  "name": "Rodopi Dent - Create Booking",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "booking-webhook",
        "responseMode": "responseNode",
        "options": {
          "allowedOrigins": "*"
        }
      },
      "id": "webhook-booking",
      "name": "Webhook - Create Booking",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// Validate incoming booking data\nconst body = $input.first().json.body;\n\nconst required = ['patientName', 'patientPhone', 'date', 'startTime'];\nconst missing = required.filter(field => !body[field]);\n\nif (missing.length > 0) {\n  return [{\n    json: {\n      success: false,\n      error: `\u041b\u0438\u043f\u0441\u0432\u0430\u0449\u0438 \u043f\u043e\u043b\u0435\u0442\u0430: ${missing.join(', ')}`,\n      valid: false\n    }\n  }];\n}\n\n// Validate phone number (Bulgarian format)\nconst phone = body.patientPhone.replace(/\\s/g, '');\nconst phoneRegex = /^(\\+359|0)[0-9]{9}$/;\nif (!phoneRegex.test(phone)) {\n  return [{\n    json: {\n      success: false,\n      error: '\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0435\u043d \u043d\u043e\u043c\u0435\u0440',\n      valid: false\n    }\n  }];\n}\n\n// Prepare data for conflict check\nreturn [{\n  json: {\n    patientName: body.patientName.trim(),\n    patientPhone: phone,\n    date: body.date,\n    startTime: body.startTime,\n    duration: parseInt(body.duration) || 30,\n    reason: body.reason || '',\n    valid: true\n  }\n}];"
      },
      "id": "validate",
      "name": "Validate Input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.valid }}",
              "value2": true
            }
          ]
        }
      },
      "id": "if-valid",
      "name": "Is Valid?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1hv4XAfHhScA40Bm1kQ3I-Ih4SJuCBpOJxTOYDNb167g"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Appointments"
        },
        "options": {
          "returnAllMatches": true
        }
      },
      "id": "sheets-read",
      "name": "Get Existing Appointments",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        660,
        -100
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Get the new booking request\nconst booking = $('Validate Input').first().json;\nconst newDate = booking.date;\nconst newStartTime = booking.startTime;\nconst newDuration = booking.duration;\n\n// Helper function to convert time to minutes\nfunction timeToMinutes(time) {\n  const [hours, minutes] = time.split(':').map(Number);\n  return hours * 60 + minutes;\n}\n\nconst newStart = timeToMinutes(newStartTime);\nconst newEnd = newStart + newDuration;\n\n// Get existing appointments for the same date\nconst appointments = $('Get Existing Appointments').all();\n\n// Check for conflicts\nlet hasConflict = false;\nlet conflictWith = null;\n\nfor (const apt of appointments) {\n  // Skip cancelled appointments\n  if (apt.json.status === 'cancelled') continue;\n  \n  // Only check same date\n  if (apt.json.date !== newDate) continue;\n  \n  const existingStart = timeToMinutes(apt.json.startTime);\n  const existingDuration = parseInt(apt.json.duration) || 30;\n  const existingEnd = existingStart + existingDuration;\n  \n  // Check for overlap:\n  // Conflict if: newStart < existingEnd AND newEnd > existingStart\n  if (newStart < existingEnd && newEnd > existingStart) {\n    hasConflict = true;\n    conflictWith = apt.json.startTime;\n    break;\n  }\n}\n\nif (hasConflict) {\n  return [{\n    json: {\n      success: false,\n      error: `\u0422\u043e\u0437\u0438 \u0447\u0430\u0441 \u0435 \u0432\u0435\u0447\u0435 \u0437\u0430\u0435\u0442 (\u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442 \u0441 ${conflictWith})`,\n      conflict: true\n    }\n  }];\n}\n\n// No conflict - generate ID and prepare for save\nconst id = 'apt_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 8);\nconst now = new Date().toISOString();\n\nreturn [{\n  json: {\n    id: id,\n    patientName: booking.patientName,\n    patientPhone: booking.patientPhone,\n    date: booking.date,\n    startTime: booking.startTime,\n    duration: booking.duration,\n    reason: booking.reason,\n    status: 'pending',\n    createdAt: now,\n    updatedAt: now,\n    conflict: false\n  }\n}];"
      },
      "id": "check-conflicts",
      "name": "Check Conflicts",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        -100
      ]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.conflict }}",
              "value2": false
            }
          ]
        }
      },
      "id": "if-no-conflict",
      "name": "No Conflict?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1100,
        -100
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1hv4XAfHhScA40Bm1kQ3I-Ih4SJuCBpOJxTOYDNb167g"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Appointments"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "id": "={{ $json.id }}",
            "patientName": "={{ $json.patientName }}",
            "patientPhone": "={{ $json.patientPhone }}",
            "date": "={{ $json.date }}",
            "startTime": "={{ $json.startTime }}",
            "duration": "={{ $json.duration }}",
            "reason": "={{ $json.reason }}",
            "status": "={{ $json.status }}",
            "createdAt": "={{ $json.createdAt }}",
            "updatedAt": "={{ $json.updatedAt }}"
          }
        },
        "options": {}
      },
      "id": "sheets-append",
      "name": "Save to Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1320,
        -200
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\n  \"success\": true,\n  \"message\": \"\u0427\u0430\u0441\u044a\u0442 \u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0437\u0430\u043f\u0430\u0437\u0435\u043d!\",\n  \"appointmentId\": \"{{ $json.id }}\",\n  \"date\": \"{{ $json.date }}\",\n  \"time\": \"{{ $json.startTime }}\"\n}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "id": "response-success",
      "name": "Respond Success",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1540,
        -200
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\n  \"success\": false,\n  \"error\": \"{{ $json.error }}\"\n}",
        "options": {
          "responseCode": "409",
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "id": "response-conflict",
      "name": "Respond Conflict",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1320,
        0
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\n  \"success\": false,\n  \"error\": \"{{ $json.error }}\"\n}",
        "options": {
          "responseCode": "400",
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "id": "response-invalid",
      "name": "Respond Invalid",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        660,
        100
      ]
    },
    {
      "parameters": {
        "from": "",
        "to": "={{ $('Check Conflicts').item.json.patientPhone }}",
        "message": "=\u0417\u0434\u0440\u0430\u0432\u0435\u0439\u0442\u0435, {{ $('Check Conflicts').item.json.patientName }}!\n\n\u0412\u0430\u0448\u0438\u044f\u0442 \u0447\u0430\u0441 \u0432 \u0420\u043e\u0434\u043e\u043f\u0438 \u0414\u0435\u043d\u0442 \u0435 \u0437\u0430\u043f\u0430\u0437\u0435\u043d:\n\ud83d\udcc5 \u0414\u0430\u0442\u0430: {{ $('Check Conflicts').item.json.date }}\n\ud83d\udd50 \u0427\u0430\u0441: {{ $('Check Conflicts').item.json.startTime }}\n\u23f1\ufe0f \u041f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e\u0441\u0442: {{ $('Check Conflicts').item.json.duration }} \u043c\u0438\u043d.\n\n\u041e\u0447\u0430\u043a\u0432\u0430\u043c\u0435 \u0412\u0438!\n\u0420\u043e\u0434\u043e\u043f\u0438 \u0414\u0435\u043d\u0442"
      },
      "id": "twilio-sms",
      "name": "Send SMS Confirmation",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [
        1540,
        -100
      ],
      "credentials": {
        "twilioApi": {
          "name": "<your credential>"
        }
      },
      "continueOnFail": true
    }
  ],
  "connections": {
    "Webhook - Create Booking": {
      "main": [
        [
          {
            "node": "Validate Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Input": {
      "main": [
        [
          {
            "node": "Is Valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Valid?": {
      "main": [
        [
          {
            "node": "Get Existing Appointments",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond Invalid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Existing Appointments": {
      "main": [
        [
          {
            "node": "Check Conflicts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Conflicts": {
      "main": [
        [
          {
            "node": "No Conflict?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "No Conflict?": {
      "main": [
        [
          {
            "node": "Save to Sheets",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond Conflict",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save to Sheets": {
      "main": [
        [
          {
            "node": "Respond Success",
            "type": "main",
            "index": 0
          },
          {
            "node": "Send SMS Confirmation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "tags": [
    {
      "name": "Rodopi Dent"
    }
  ]
}