{
  "name": "ALLSoft — Agendar citas por mensaje",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "agendar",
        "responseMode": "responseNode",
        "options": {}
      },
      "name": "Llega un mensaje",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        -680,
        300
      ],
      "webhookId": "a6e4d4r-0000-4000-8000-000000000001",
      "notes": "Conéctalo a tu proveedor de WhatsApp o a un formulario. Espera { mensaje, de }.",
      "id": "ag3n-0001-4000-8000-000000000001"
    },
    {
      "parameters": {
        "jsCode": "// La agenda real del negocio. Sustituye por Google Calendar, tu sistema de\n// citas o una hoja de cálculo.\nconst ocupado = [\n  { dia: 'lunes',     hora: '09:00' },\n  { dia: 'lunes',     hora: '10:00' },\n  { dia: 'martes',    hora: '15:00' },\n  { dia: 'miércoles', hora: '09:00' },\n  { dia: 'miércoles', hora: '10:00' },\n  { dia: 'miércoles', hora: '11:00' },\n];\n\nconst horarioAtencion = ['09:00', '10:00', '11:00', '14:00', '15:00', '16:00'];\nconst diasHabiles = ['lunes', 'martes', 'miércoles', 'jueves', 'viernes'];\n\nconst servicios = ['corte', 'tinte', 'manicure', 'pedicure', 'peinado'];\n\n// El mensaje llega dentro de body cuando entra por webhook\nconst entrada = $input.first().json;\nconst mensaje = entrada.body?.mensaje || entrada.mensaje || '';\nconst de = entrada.body?.de || entrada.de || 'cliente';\n\nreturn [{\n  json: {\n    mensaje,\n    de,\n    ocupado,\n    horarioAtencion,\n    diasHabiles,\n    servicios,\n  },\n}];\n"
      },
      "name": "Agenda y horarios",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -460,
        300
      ],
      "notes": "Reemplaza por Google Calendar o tu sistema de citas. Aquí vive la agenda REAL.",
      "id": "ag3n-0002-4000-8000-000000000002"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.groq.com/openai/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer TU_GROQ_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: 'llama-3.3-70b-versatile', temperature: 0, max_tokens: 150, response_format: { type: 'json_object' }, messages: [ { role: 'system', content: 'Lees un mensaje de un cliente que quiere agendar. Devuelve SOLO un JSON con esta forma: {\"servicio\":\"\",\"dia\":\"\",\"hora\":\"\"}. dia en minusculas (lunes a viernes). hora en formato HH:MM de 24 horas. Si algo no se menciona, dejalo vacio. NO confirmes ni rechaces nada: solo extrae lo que pidio.' }, { role: 'user', content: $json.mensaje } ] }) }}",
        "options": {}
      },
      "name": "Entender qué pide (Groq)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -220,
        300
      ],
      "onError": "continueErrorOutput",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "notes": "Solo EXTRAE lo que pidió el cliente. La disponibilidad la decide el código, no el modelo.",
      "id": "ag3n-0003-4000-8000-000000000003"
    },
    {
      "parameters": {
        "jsCode": "// Cruza lo que pidió el cliente contra la agenda REAL.\n//\n// REGLA DE ORO: el modelo solo entiende qué pidió el cliente. Nunca confirma\n// ni descarta un horario — eso lo decide este código mirando la agenda. Si\n// dejaras que el modelo confirmara, algún día agendaría dos personas a la\n// misma hora, o inventaría un horario que no existe.\nconst base = $('Agenda y horarios').first().json;\n\nlet pedido = {};\ntry {\n  pedido = JSON.parse($input.first().json.choices[0].message.content);\n} catch (e) {\n  pedido = {};\n}\n\nconst dia = String(pedido.dia || '').toLowerCase().trim();\nconst hora = String(pedido.hora || '').trim();\nconst servicio = String(pedido.servicio || '').toLowerCase().trim();\n\nconst estaOcupado = (d, h) => base.ocupado.some((o) => o.dia === d && o.hora === h);\nconst diaValido = base.diasHabiles.includes(dia);\nconst horaValida = base.horarioAtencion.includes(hora);\n\nconst disponible = diaValido && horaValida && !estaOcupado(dia, hora);\n\n// Alternativas reales del mismo día; si no hay, del día siguiente hábil\nlet alternativas = [];\nif (!disponible && diaValido) {\n  alternativas = base.horarioAtencion.filter((h) => !estaOcupado(dia, h)).slice(0, 3);\n}\nif (alternativas.length === 0) {\n  const siguiente = base.diasHabiles[(base.diasHabiles.indexOf(dia) + 1) % 5];\n  alternativas = base.horarioAtencion\n    .filter((h) => !estaOcupado(siguiente, h))\n    .slice(0, 3)\n    .map((h) => `${siguiente} a las ${h}`);\n} else {\n  alternativas = alternativas.map((h) => `${dia} a las ${h}`);\n}\n\nlet motivo = '';\nif (!dia || !hora) motivo = 'no_entendi';\nelse if (!diaValido) motivo = 'dia_no_habil';\nelse if (!horaValida) motivo = 'fuera_de_horario';\nelse if (estaOcupado(dia, hora)) motivo = 'ocupado';\n\nreturn [{\n  json: {\n    de: base.de,\n    mensaje: base.mensaje,\n    servicio: base.servicios.includes(servicio) ? servicio : '',\n    dia,\n    hora,\n    disponible,\n    motivo,\n    alternativas,\n  },\n}];\n"
      },
      "name": "Verificar disponibilidad",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        20,
        200
      ],
      "notes": "Cruza lo pedido contra la agenda real y propone alternativas que SÍ existen.",
      "id": "ag3n-0004-4000-8000-000000000004"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "version": 2
          },
          "conditions": [
            {
              "id": "disp1",
              "leftValue": "={{ $json.disponible }}",
              "rightValue": "={{ true }}",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "name": "¿Hay cupo?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        240,
        200
      ],
      "id": "ag3n-0005-4000-8000-000000000005"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c1",
              "name": "respuesta",
              "value": "={{ 'Listo, te agendé el ' + $json.dia + ' a las ' + $json.hora + '. Te esperamos.' }}",
              "type": "string"
            },
            {
              "id": "c2",
              "name": "estado",
              "value": "agendada",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "name": "Confirmar cita",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        480,
        120
      ],
      "notes": "Aquí engancha el nodo que escribe la cita en tu calendario.",
      "id": "ag3n-0006-4000-8000-000000000006"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "n1",
              "name": "respuesta",
              "value": "={{ 'Ese horario no está libre. Te puedo ofrecer: ' + $json.alternativas.join(', ') + '. ¿Cuál te sirve?' }}",
              "type": "string"
            },
            {
              "id": "n2",
              "name": "estado",
              "value": "propuso_alternativas",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "name": "Proponer otros horarios",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        480,
        300
      ],
      "notes": "Las alternativas salen de la agenda real, no del modelo.",
      "id": "ag3n-0007-4000-8000-000000000007"
    },
    {
      "parameters": {
        "respondWith": "text",
        "responseBody": "={{ $json.respuesta }}",
        "options": {}
      },
      "name": "Responder al cliente",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        720,
        200
      ],
      "id": "ag3n-0008-4000-8000-000000000008"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "e1",
              "name": "estado",
              "value": "error_ia",
              "type": "string"
            },
            {
              "id": "e2",
              "name": "detalle",
              "value": "={{ $json.error || 'Groq no respondió' }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "name": "Avisar del fallo",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        20,
        440
      ],
      "notes": "Salida de error del nodo de IA: el flujo no se cae, avisa.",
      "id": "ag3n-0009-4000-8000-000000000009"
    }
  ],
  "connections": {
    "Llega un mensaje": {
      "main": [
        [
          {
            "node": "Agenda y horarios",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Agenda y horarios": {
      "main": [
        [
          {
            "node": "Entender qué pide (Groq)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Entender qué pide (Groq)": {
      "main": [
        [
          {
            "node": "Verificar disponibilidad",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Avisar del fallo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verificar disponibilidad": {
      "main": [
        [
          {
            "node": "¿Hay cupo?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "¿Hay cupo?": {
      "main": [
        [
          {
            "node": "Confirmar cita",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Proponer otros horarios",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Confirmar cita": {
      "main": [
        [
          {
            "node": "Responder al cliente",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Proponer otros horarios": {
      "main": [
        [
          {
            "node": "Responder al cliente",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "instanceId": "allsoft-academia"
  },
  "tags": []
}
