{
 "name": "وكيل الحجوزات — اسأل أبو خليفة",
 "nodes": [
  {
   "parameters": {
    "updates": [
     "messages"
    ],
    "options": {}
   },
   "id": "cf1b6fbd-9d41-4125-923f-186bb8eb4929",
   "name": "رسالة واتساب",
   "type": "n8n-nodes-base.whatsAppTrigger",
   "typeVersion": 1,
   "position": [
    -660,
    0
   ],
   "credentials": {
    "whatsAppTriggerApi": {
     "id": "",
     "name": "WhatsApp OAuth (App ID + App Secret)"
    }
   }
  },
  {
   "parameters": {
    "jsCode": "// نطلع رقم الزبون واسمه ونص رسالته من واتساب\nconst v = $input.first().json; const m = v.messages?.[0];\nif (!m || m.type !== 'text') return [];\nreturn [{ json: { customer: m.from, customer_name: v.contacts?.[0]?.profile?.name || '', text: m.text.body, phone_number_id: v.metadata.phone_number_id } }];"
   },
   "id": "9780d20a-efc0-489c-ac77-a554552e1120",
   "name": "تجهيز الرسالة",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    -440,
    0
   ]
  },
  {
   "parameters": {
    "assignments": {
     "assignments": [
      {
       "id": "bd6f422c-aafb-4d51-8350-561fa324f38b",
       "name": "business",
       "type": "string",
       "value": "عيادة ابتسامة لطب الأسنان"
      },
      {
       "id": "80f39f65-68a2-4600-b1f4-9863796e3a16",
       "name": "services",
       "type": "string",
       "value": "كشف، تنظيف أسنان، حشوة، تبييض"
      },
      {
       "id": "1643af5f-e098-4fb6-b12f-a38ca3b67f95",
       "name": "shifts",
       "type": "string",
       "value": "9:00-13:00, 16:00-21:00"
      },
      {
       "id": "e44ae726-0e11-49af-93fa-8cc20db9e939",
       "name": "closed_days",
       "type": "string",
       "value": "الجمعة"
      },
      {
       "id": "af71949c-5368-4636-b6dc-32920e3615cd",
       "name": "slot_minutes",
       "type": "number",
       "value": 30
      },
      {
       "id": "8a12a065-c4a6-4d3f-ab86-2ee4dd17fe7c",
       "name": "days_ahead",
       "type": "number",
       "value": 7
      },
      {
       "id": "f465e17a-bb1e-4f94-a16a-a9bf91eda990",
       "name": "human_phone",
       "type": "string",
       "value": "0500000000"
      }
     ]
    },
    "includeOtherFields": false,
    "options": {}
   },
   "id": "60b066e6-2488-4701-a4d1-37ab7b173fac",
   "name": "الإعدادات",
   "type": "n8n-nodes-base.set",
   "typeVersion": 3.4,
   "position": [
    -220,
    0
   ]
  },
  {
   "parameters": {
    "operation": "getAll",
    "calendar": {
     "__rl": true,
     "mode": "list",
     "value": "primary"
    },
    "returnAll": true,
    "timeMin": "={{ $now.toISO() }}",
    "timeMax": "={{ $now.plus({ days: $('الإعدادات').first().json.days_ahead + 1 }).toISO() }}",
    "options": {
     "singleEvents": true
    }
   },
   "id": "5c1527d3-9226-448d-84f2-f5ca42fcf085",
   "name": "المواعيد المحجوزة (Google Calendar)",
   "type": "n8n-nodes-base.googleCalendar",
   "typeVersion": 1.3,
   "position": [
    0,
    0
   ],
   "credentials": {
    "googleCalendarOAuth2Api": {
     "id": "",
     "name": "Google Calendar"
    }
   },
   "alwaysOutputData": true
  },
  {
   "parameters": {
    "jsCode": "// يحسب المواعيد الفاضية للأيام الجاية من دوام العيادة، ويشيل المواعيد المحجوزة في التقويم\nconst S = $('الإعدادات').first().json;\nconst busy = $input.all().map(i => i.json).filter(e => e.start)\n  .map(e => ({ s: new Date(e.start.dateTime || e.start), e: new Date(e.end.dateTime || e.end), id: e.id, desc: e.description || '', summary: e.summary || '' }));\nconst now = $('تجهيز الرسالة').first().json.now ? DateTime.fromISO($('تجهيز الرسالة').first().json.now, { zone: 'Asia/Riyadh' }) : $now.setZone('Asia/Riyadh');\n// الدوام من الإعدادات: \"9:00-13:00, 16:00-21:00\" وأيام الإغلاق \"الجمعة\"\nconst en = x => String(x).replace(/[٠-٩]/g, d => '٠١٢٣٤٥٦٧٨٩'.indexOf(d));   // ٩ → 9\nconst toMin = x => { const [h, m] = en(x).trim().split(':'); return Number(h) * 60 + Number(m || 0); };\nconst shifts = String(S.shifts).split(/[,،]/).filter(x => x.includes('-')).map(x => x.split('-').map(toMin));\nconst names = ['', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت', 'الأحد'];\nconst bare = x => x.trim().replace(/[أإآ]/g, 'ا').replace(/^ال/, '');\nconst closed = String(S.closed_days || '').split(/[,،و]/).map(x => names.map(bare).indexOf(bare(x))).filter(i => i > 0);\nconst slots = [];\nfor (let d = 0; d <= S.days_ahead; d++) {\n  const day = now.plus({ days: d }).startOf('day');\n  if (closed.includes(day.weekday)) continue;\n  for (const [a, b] of shifts) {\n    for (let t = day.plus({ minutes: a }); t.plus({ minutes: S.slot_minutes }) <= day.plus({ minutes: b }); t = t.plus({ minutes: S.slot_minutes })) {\n      if (t <= now.plus({ minutes: 30 })) continue;\n      const s = t.toJSDate(), e = t.plus({ minutes: S.slot_minutes }).toJSDate();\n      if (busy.some(x => x.s < e && x.e > s)) continue;\n      slots.push(t.toFormat(\"yyyy-LL-dd'T'HH:mm\"));\n    }\n  }\n}\n// نص مختصر للذكاء الاصطناعي: كل يوم في سطر\nconst byDay = {};\nfor (const s of slots) { const k = s.slice(0, 10); (byDay[k] = byDay[k] || []).push(s.slice(11)); }\nconst text = Object.entries(byDay).map(([k, v]) => `${names[DateTime.fromISO(k).weekday]} ${k}: ${v.join('، ')}`).join('\\n') || 'ما فيه مواعيد فاضية في الأيام الجاية.';\nconst phone = $('تجهيز الرسالة').first().json.customer;\nconst mine = busy.filter(x => x.desc.includes(phone)).map(x => ({ id: x.id, start: DateTime.fromJSDate(x.s).setZone('Asia/Riyadh').toFormat(\"yyyy-LL-dd'T'HH:mm\") }));\nconst hours_text = 'الفترات: ' + S.shifts + (closed.length ? ' · مغلق: ' + closed.map(i => names[i]).join('، ') : '');\nreturn [{ json: { slots, slots_text: text, my_appointments: mine, hours_text,\n  now_text: now.setLocale('ar').toFormat(\"cccc yyyy-LL-dd HH:mm\") } }];"
   },
   "id": "e9667d13-5194-43e6-9c41-5e0a163d5ea0",
   "name": "المواعيد الفاضية",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    220,
    0
   ]
  },
  {
   "parameters": {
    "promptType": "define",
    "text": "={{ $('تجهيز الرسالة').first().json.text }}",
    "hasOutputParser": true,
    "options": {
     "systemMessage": "=أنت موظف الحجوزات في «{{ $('الإعدادات').first().json.business }}» على واتساب. ترد بلهجة سعودية بيضاء ومهذبة، باختصار (سطرين أو ثلاثة)، وبنفس لغة الزبون إذا كتب بالإنجليزي.\n\nالوقت الحين: {{ $('المواعيد الفاضية').first().json.now_text }} (بتوقيت الرياض)\nالخدمات: {{ $('الإعدادات').first().json.services }}\nالدوام: {{ $('المواعيد الفاضية').first().json.hours_text }}\nمدة الموعد: {{ $('الإعدادات').first().json.slot_minutes }} دقيقة\n\nالمواعيد الفاضية (هذا المصدر الوحيد، لا تقترح غيرها):\n{{ $('المواعيد الفاضية').first().json.slots_text }}\n\nمواعيد هذا الزبون في التقويم الحين: {{ $('المواعيد الفاضية').first().json.my_appointments.length ? JSON.stringify($('المواعيد الفاضية').first().json.my_appointments) : 'ما عنده ولا موعد' }}\n(التقويم هو الصح دايماً. إذا المحادثة السابقة تقول إن عنده موعد وهو مو في التقويم، يعني انلغى أو تغيّر: لا تقول له إنه محجوز، وتعامل معه كأنه ما عنده موعد.)\n\nالقواعد:\n1. إذا الزبون طلب وقت فاضي وواضح (يوم + ساعة) وعطاك اسمه: action = \"book\" و slot بالصيغة YYYY-MM-DDTHH:mm من القائمة بالضبط.\n2. إذا الوقت مو في القائمة (محجوز أو برا الدوام أو الجمعة): لا تحجز. قل له إنه مو متاح واقترح أقرب 3 مواعيد فاضية. action = \"reply\".\n3. إذا ما عطاك اسمه وطلب يحجز: اسأله عن اسمه وثبّت الوقت. action = \"reply\".\n4. إذا طلب يلغي وعنده موعد: action = \"cancel\" و slot = موعده.\n5. الأسعار وأي معلومة مو مكتوبة فوق: لا تخمّن. قل له إن الموظف يجاوبه على الرقم {{ $('الإعدادات').first().json.human_phone }}، ولا توعده إن أحد بيتواصل معه. action = \"human\".\n6. إذا ذكر ألم شديد أو تورم أو نزيف أو حالة طارئة: انصحه يتصل بالعيادة على {{ $('الإعدادات').first().json.human_phone }} أو يروح الطوارئ إذا الحالة شديدة، واقترح أقرب موعد فاضي. action = \"human\".\n7. لا تشخّص ولا تعطي نصيحة طبية.\n8. نادِ الزبون باسمه مثل ما كتبه بالضبط، لا تزيد عليه («عبدالله» تبقى «عبدالله»).\n9. اكتب الأوقات بنظام 12 ساعة مع الصبح أو الظهر أو العصر أو المساء (مثل: 5:30 المساء).\n10. إذا الوقت الحين بين 12 بالليل و5 الفجر والزبون قال «بكرة» أو «اليوم»: الكلمة محيرة. لا تحجز، اسأله بالتاريخ: «تقصد [اليوم اللي يبدأ الصبح باسمه وتاريخه] ولا [اليوم اللي بعده باسمه وتاريخه]؟». action = \"reply\".\n\nأعد الرد فقط بالصيغة المطلوبة (JSON)."
    }
   },
   "id": "d852d17c-acf2-4ec5-ae60-5782f6610785",
   "name": "موظف الحجوزات",
   "type": "@n8n/n8n-nodes-langchain.agent",
   "typeVersion": 3.1,
   "position": [
    440,
    0
   ],
   "retryOnFail": true,
   "maxTries": 2,
   "waitBetweenTries": 1000,
   "onError": "continueErrorOutput"
  },
  {
   "parameters": {
    "jsCode": "const m = $('تجهيز الرسالة').first().json; const S = $('الإعدادات').first().json;\nreturn [{ json: { action: 'reply', customer: m.customer, event_id: '', start: '', end: '', name: '',\n  reply: 'آسفين، ما قدرت أرد على رسالتك الحين. ممكن تعيد كتابتها؟ أو اتصل علينا على ' + S.human_phone } }];"
   },
   "id": "ddac0224-6a99-4c92-a3a5-ba5dfc3c0e07",
   "name": "رد احتياطي",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    660,
    380
   ]
  },
  {
   "parameters": {
    "model": {
     "__rl": true,
     "mode": "id",
     "value": "claude-haiku-5-5"
    },
    "options": {
     "maxTokensToSample": 1500
    }
   },
   "id": "238855fc-47b5-4eaf-bbcd-9e065ee4f119",
   "name": "Claude Haiku",
   "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
   "typeVersion": 1.6,
   "position": [
    360,
    220
   ],
   "credentials": {
    "anthropicApi": {
     "id": "",
     "name": "Anthropic API"
    }
   }
  },
  {
   "parameters": {
    "sessionIdType": "customKey",
    "sessionKey": "={{ $('تجهيز الرسالة').first().json.customer }}",
    "contextWindowLength": 10
   },
   "id": "f3091652-28fe-482c-aa45-3625c700466d",
   "name": "ذاكرة المحادثة",
   "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
   "typeVersion": 1.3,
   "position": [
    500,
    220
   ]
  },
  {
   "parameters": {
    "schemaType": "fromJson",
    "jsonSchemaExample": "{\"action\": \"reply\", \"slot\": \"\", \"name\": \"\", \"reply\": \"هلا والله، عندنا بكرة 5:30 أو 6:00 العصر، أي وقت يناسبك؟ وعطني اسمك الكريم.\"}"
   },
   "id": "c3831965-4782-41c8-8c74-6393c091c989",
   "name": "صيغة الرد",
   "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
   "typeVersion": 1.3,
   "position": [
    640,
    220
   ]
  },
  {
   "parameters": {
    "jsCode": "// نتأكد بأنفسنا: الحجز لازم يكون في وقت فاضي فعلاً، والإلغاء لازم يكون موعد الزبون نفسه\nconst ai = $input.first().json.output;\nconst f = $('المواعيد الفاضية').first().json;\nconst S = $('الإعدادات').first().json;\nconst m = $('تجهيز الرسالة').first().json;\nlet action = ai.action, reply = ai.reply, event_id = '', start = '', end = '';\nif (action === 'book') {\n  if (!f.slots.includes(ai.slot)) {\n    action = 'reply';\n    reply = 'آسفين، هذا الوقت مو متاح. أقرب مواعيدنا: ' + f.slots.slice(0, 3).map(s => s.replace('T', ' ')).join('، ') + '. أي وقت يناسبك؟';\n  } else {\n    start = ai.slot + ':00+03:00';\n    end = DateTime.fromISO(ai.slot, { zone: 'Asia/Riyadh' }).plus({ minutes: S.slot_minutes }).toFormat(\"yyyy-LL-dd'T'HH:mm:ss\") + '+03:00';\n  }\n}\nif (action === 'cancel') {\n  const list = f.my_appointments;\n  const mine = list.find(a => a.start === ai.slot) || (list.length === 1 ? list[0] : null);\n  if (!list.length) { action = 'reply'; reply = 'ما لقيت لك موعد مسجّل بهالرقم. تقدر تعطيني اليوم والوقت؟'; }\n  else if (!mine) { action = 'reply'; reply = 'عندك أكثر من موعد: ' + list.map(a => a.start.replace('T', ' ')).join('، ') + '. أي واحد تبي تلغي؟'; }\n  else event_id = mine.id;\n}\nreturn [{ json: { action, reply, event_id, start, end, name: ai.name || m.customer_name || '', customer: m.customer,\n  summary: `موعد: ${ai.name || m.customer_name || 'زبون'}`, description: `واتساب: ${m.customer}\\nحجز آلي من وكيل الحجوزات` } }];"
   },
   "id": "66e1e421-f032-454d-82c3-5a5b0b085451",
   "name": "تأكد من الوقت",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    660,
    0
   ]
  },
  {
   "parameters": {
    "rules": {
     "values": [
      {
       "conditions": {
        "options": {
         "caseSensitive": true,
         "leftValue": "",
         "typeValidation": "loose",
         "version": 2
        },
        "conditions": [
         {
          "id": "afc94a80-5c6f-4a5d-ab4a-2653d7137e9b",
          "leftValue": "={{ $json.action }}",
          "rightValue": "book",
          "operator": {
           "type": "string",
           "operation": "equals"
          }
         }
        ],
        "combinator": "and"
       },
       "renameOutput": true,
       "outputKey": "book"
      },
      {
       "conditions": {
        "options": {
         "caseSensitive": true,
         "leftValue": "",
         "typeValidation": "loose",
         "version": 2
        },
        "conditions": [
         {
          "id": "9b4a607f-37e2-403d-8d03-78dbf337389f",
          "leftValue": "={{ $json.action }}",
          "rightValue": "cancel",
          "operator": {
           "type": "string",
           "operation": "equals"
          }
         }
        ],
        "combinator": "and"
       },
       "renameOutput": true,
       "outputKey": "cancel"
      }
     ]
    },
    "options": {
     "fallbackOutput": "extra"
    }
   },
   "id": "deb23cce-dfab-4f95-846e-d24fbc1d244f",
   "name": "حجز ولا إلغاء؟",
   "type": "n8n-nodes-base.switch",
   "typeVersion": 3.2,
   "position": [
    880,
    0
   ]
  },
  {
   "parameters": {
    "calendar": {
     "__rl": true,
     "mode": "list",
     "value": "primary"
    },
    "start": "={{ $json.start }}",
    "end": "={{ $json.end }}",
    "additionalFields": {
     "summary": "={{ $json.summary }}",
     "description": "={{ $json.description }}"
    }
   },
   "id": "f7b6ffa7-f3f5-49a5-8205-fb5d37b41d9d",
   "name": "احجز في التقويم",
   "type": "n8n-nodes-base.googleCalendar",
   "typeVersion": 1.3,
   "position": [
    1100,
    -160
   ],
   "credentials": {
    "googleCalendarOAuth2Api": {
     "id": "",
     "name": "Google Calendar"
    }
   }
  },
  {
   "parameters": {
    "operation": "delete",
    "calendar": {
     "__rl": true,
     "mode": "list",
     "value": "primary"
    },
    "eventId": "={{ $json.event_id }}",
    "options": {}
   },
   "id": "ecd05bf0-d621-4755-9b38-164a55b53ef0",
   "name": "ألغِ من التقويم",
   "type": "n8n-nodes-base.googleCalendar",
   "typeVersion": 1.3,
   "position": [
    1100,
    0
   ],
   "credentials": {
    "googleCalendarOAuth2Api": {
     "id": "",
     "name": "Google Calendar"
    }
   }
  },
  {
   "parameters": {
    "resource": "message",
    "operation": "send",
    "phoneNumberId": "={{ $('تجهيز الرسالة').first().json.phone_number_id }}",
    "recipientPhoneNumber": "={{ $('تأكد من الوقت').first().json.customer }}",
    "textBody": "={{ $('تأكد من الوقت').first().json.reply }}",
    "additionalFields": {}
   },
   "id": "eae75d0c-4497-4049-949d-7c143ca9f709",
   "name": "الرد على واتساب",
   "type": "n8n-nodes-base.whatsApp",
   "typeVersion": 1.1,
   "position": [
    1320,
    0
   ],
   "credentials": {
    "whatsAppApi": {
     "id": "",
     "name": "WhatsApp (Access token + Business Account ID)"
    }
   }
  },
  {
   "parameters": {
    "resource": "message",
    "operation": "send",
    "phoneNumberId": "={{ $('تجهيز الرسالة').first().json.phone_number_id }}",
    "recipientPhoneNumber": "={{ $json.customer }}",
    "textBody": "={{ $json.reply }}",
    "additionalFields": {}
   },
   "id": "001b8512-a431-463d-8cc6-83c4d3c5bafa",
   "name": "رد احتياطي على واتساب",
   "type": "n8n-nodes-base.whatsApp",
   "typeVersion": 1.1,
   "position": [
    880,
    380
   ],
   "credentials": {
    "whatsAppApi": {
     "id": "",
     "name": "WhatsApp (Access token + Business Account ID)"
    }
   }
  }
 ],
 "connections": {
  "رسالة واتساب": {
   "main": [
    [
     {
      "node": "تجهيز الرسالة",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "تجهيز الرسالة": {
   "main": [
    [
     {
      "node": "الإعدادات",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "الإعدادات": {
   "main": [
    [
     {
      "node": "المواعيد المحجوزة (Google Calendar)",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "المواعيد المحجوزة (Google Calendar)": {
   "main": [
    [
     {
      "node": "المواعيد الفاضية",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "المواعيد الفاضية": {
   "main": [
    [
     {
      "node": "موظف الحجوزات",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "موظف الحجوزات": {
   "main": [
    [
     {
      "node": "تأكد من الوقت",
      "type": "main",
      "index": 0
     }
    ],
    [
     {
      "node": "رد احتياطي",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Claude Haiku": {
   "ai_languageModel": [
    [
     {
      "node": "موظف الحجوزات",
      "type": "ai_languageModel",
      "index": 0
     }
    ]
   ]
  },
  "ذاكرة المحادثة": {
   "ai_memory": [
    [
     {
      "node": "موظف الحجوزات",
      "type": "ai_memory",
      "index": 0
     }
    ]
   ]
  },
  "صيغة الرد": {
   "ai_outputParser": [
    [
     {
      "node": "موظف الحجوزات",
      "type": "ai_outputParser",
      "index": 0
     }
    ]
   ]
  },
  "تأكد من الوقت": {
   "main": [
    [
     {
      "node": "حجز ولا إلغاء؟",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "حجز ولا إلغاء؟": {
   "main": [
    [
     {
      "node": "احجز في التقويم",
      "type": "main",
      "index": 0
     }
    ],
    [
     {
      "node": "ألغِ من التقويم",
      "type": "main",
      "index": 0
     }
    ],
    [
     {
      "node": "الرد على واتساب",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "احجز في التقويم": {
   "main": [
    [
     {
      "node": "الرد على واتساب",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "ألغِ من التقويم": {
   "main": [
    [
     {
      "node": "الرد على واتساب",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "رد احتياطي": {
   "main": [
    [
     {
      "node": "رد احتياطي على واتساب",
      "type": "main",
      "index": 0
     }
    ]
   ]
  }
 },
 "settings": {
  "executionOrder": "v1",
  "timezone": "Asia/Riyadh"
 },
 "pinData": {}
}