เพิ่มการแจ้งเตือน SMS ให้เว็บจอง AI ของคุณ

ส่ง SMS อัตโนมัติเมื่อมีคนส่งคำขอจองเข้ามา

แผนการทำงานและแหล่งข้อมูล

เพิ่งเริ่มใช่ไหม? เริ่มตรงนี้ก่อน

คู่มือนี้เป็นการอัปเกรดโปรเจกต์เว็บจอง AI ด้วยการแจ้งเตือน SMS อัตโนมัติ ถ้ายังไม่ได้สร้างเว็บจอง ให้เริ่มจากคู่มือหลักก่อน

สร้างเว็บจองคิวด้วย AI

เปิดคู่มือหลัก

MAIN GUIDE

https://profitstudio.app/video/build-booking-website-ai

ดึงข้อมูล Twilio ของคุณ

สร้างบัญชี Twilio

เปิด Twilio

https://www.twilio.com/

ดึงค่าเหล่านี้จากแดชบอร์ด Twilio ของคุณ:

ค่าต่าง ๆ ของ Twilio

TWILIO_ACCOUNT_SID TWILIO_AUTH_TOKEN TWILIO_PHONE_NUMBER

TWILIO_ACCOUNT_SID

Account SID

TWILIO_AUTH_TOKEN

Auth Token

TWILIO_PHONE_NUMBER

หมายเลขโทรศัพท์ Twilio

เลือกเบอร์ Twilio ที่รองรับ SMS

เพิ่ม Secrets ใน Supabase

เปิด Supabase Edge Function Secrets

เปิด Supabase

https://supabase.com/dashboard/projects

Supabase → Project Settings → Edge Functions → Secrets

เพิ่ม secret ของ Twilio ที่จำเป็น

Secrets ที่จำเป็น

TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx TWILIO_AUTH_TOKEN=your_auth_token TWILIO_PHONE_NUMBER=+1234567890

SMS ถึงลูกค้าเปิดใช้งานเป็นค่าเริ่มต้น ส่วน SMS ถึงเจ้าของร้านเป็นตัวเลือกเสริมและปิดไว้เป็นค่าเริ่มต้น

ไม่บังคับ: ส่ง SMS ถึงเจ้าของร้าน

SMS ถึงเจ้าของร้าน (ไม่บังคับ)

SEND_OWNER_SMS=true OWNER_PHONE_NUMBER=+1234567890

เพิ่มค่าเหล่านี้เฉพาะเมื่อคุณต้องการให้เจ้าของร้านได้รับ SMS ตอนมีคำขอจองใหม่ด้วย

สร้าง Edge Function สำหรับ SMS

สร้าง Edge Function ใหม่โดยใช้ชื่อนี้เป๊ะ ๆ: send-sms-notification

ตรวจทานเทมเพลตข้อความ SMS

คัดลอกโค้ดฟังก์ชัน SMS ที่สร้างขึ้นแล้ววาง

ฟังก์ชันแจ้งเตือน SMS

// supabase/functions/send-sms-notification/index.ts // // Sends SMS notifications when a new booking request is created. // Default: sends booking request SMS to the customer. // Optional: also sends SMS notification to the business owner. // // Required Supabase Secrets: // TWILIO_ACCOUNT_SID // TWILIO_AUTH_TOKEN // TWILIO_PHONE_NUMBER // // Optional Owner SMS Secrets: // SEND_OWNER_SMS=true // OWNER_PHONE_NUMBER=+1234567890 const CORS_HEADERS = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", }; // ===== CUSTOMIZE THESE VALUES ===== const BUSINESS_NAME = "Your Business Name"; const CUSTOMER_SMS_TEMPLATE = "Hi {{customer_name}}, we received your {{service_name}} booking request for {{appointment_date}} at {{appointment_time}}. We'll contact you soon to confirm."; const OWNER_SMS_TEMPLATE = "New booking request: {{customer_name}} requested {{service_name}} on {{appointment_date}} at {{appointment_time}}. Phone: {{customer_phone}}"; // Default booking field names used in this guide. // If your project uses different field names, update them here. const FIELD_MAP = { customerName: ["full_name", "customer_name", "name"], customerPhone: ["phone", "phone_number", "customer_phone"], appointmentDate: ["appointment_date", "date", "booking_date"], appointmentTime: ["start_time", "appointment_time", "time"], serviceName: ["service_name", "service"], serviceId: ["service_id"], }; // ===== END CUSTOMIZATION ===== function json(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { ...CORS_HEADERS, "Content-Type": "application/json" }, }); } function getEnvFlag(name: string, defaultValue = false): boolean { const value = Deno.env.get(name); if (!value) return defaultValue; return value.toLowerCase() === "true"; } function getField(record: Record<string, unknown>, keys: string[]): string { for (const key of keys) { const value = record[key]; if (typeof value === "string" && value.trim()) { return value.trim(); } } return ""; } function formatTime(time: string): string { if (!time) return ""; return time.length >= 5 ? time.slice(0, 5) : time; } function renderTemplate( template: string, values: Record<string, string>, ): string { return template.replace(/\{\{(.*?)\}\}/g, (_, key) => { return values[key.trim()] ?? ""; }); } async function fetchServiceName( serviceId: string, supabaseUrl: string, serviceRoleKey: string, ): Promise<string> { try { const url = `${supabaseUrl}/rest/v1/services?id=eq.${encodeURIComponent( serviceId, )}&select=name`; const response = await fetch(url, { headers: { apikey: serviceRoleKey, Authorization: `Bearer ${serviceRoleKey}`, }, }); if (!response.ok) return ""; const rows = await response.json(); return rows?.[0]?.name ?? ""; } catch { return ""; } } async function sendTwilioSms(options: { accountSid: string; authToken: string; from: string; to: string; body: string; }) { const { accountSid, authToken, from, to, body } = options; const url = `https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json`; const form = new URLSearchParams({ From: from, To: to, Body: body, }); const response = await fetch(url, { method: "POST", headers: { Authorization: "Basic " + btoa(`${accountSid}:${authToken}`), "Content-Type": "application/x-www-form-urlencoded", }, body: form.toString(), }); const data = await response.json().catch(() => null); return { ok: response.ok, status: response.status, data, }; } Deno.serve(async (req) => { if (req.method === "OPTIONS") { return new Response("ok", { headers: CORS_HEADERS }); } if (req.method !== "POST") { return json({ success: false, error: "Method not allowed" }, 405); } const accountSid = Deno.env.get("TWILIO_ACCOUNT_SID"); const authToken = Deno.env.get("TWILIO_AUTH_TOKEN"); const twilioPhoneNumber = Deno.env.get("TWILIO_PHONE_NUMBER"); const sendCustomerSms = getEnvFlag("SEND_CUSTOMER_SMS", true); const sendOwnerSms = getEnvFlag("SEND_OWNER_SMS", false); const ownerPhoneNumber = Deno.env.get("OWNER_PHONE_NUMBER"); if (!accountSid || !authToken || !twilioPhoneNumber) { return json( { success: false, error: "Missing Twilio secrets. Please add TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER.", }, 500, ); } let payload; try { payload = await req.json(); } catch { return json({ success: false, error: "Invalid JSON body" }, 400); } const record = payload?.record; if (!record) { return json({ success: false, error: "Missing record in payload" }, 400); } const customerName = getField(record, FIELD_MAP.customerName) || "Customer"; const customerPhone = getField(record, FIELD_MAP.customerPhone); const appointmentDate = getField(record, FIELD_MAP.appointmentDate) || "your selected date"; const appointmentTime = formatTime(getField(record, FIELD_MAP.appointmentTime)) || "your selected time"; let serviceName = getField(record, FIELD_MAP.serviceName) || "your service"; const serviceId = getField(record, FIELD_MAP.serviceId); const supabaseUrl = Deno.env.get("SUPABASE_URL"); const serviceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"); if (serviceId && supabaseUrl && serviceRoleKey) { const fetchedServiceName = await fetchServiceName( serviceId, supabaseUrl, serviceRoleKey, ); if (fetchedServiceName) { serviceName = fetchedServiceName; } } const templateValues = { business_name: BUSINESS_NAME, customer_name: customerName, customer_phone: customerPhone || "Not provided", service_name: serviceName, appointment_date: appointmentDate, appointment_time: appointmentTime, }; const results = { customer_sms: "not_sent", owner_sms: "not_sent", errors: [] as unknown[], }; if (sendCustomerSms) { if (!customerPhone) { results.customer_sms = "skipped_missing_customer_phone"; } else { const customerMessage = renderTemplate( CUSTOMER_SMS_TEMPLATE, templateValues, ); const customerResult = await sendTwilioSms({ accountSid, authToken, from: twilioPhoneNumber, to: customerPhone, body: customerMessage, }); if (!customerResult.ok) { results.customer_sms = "failed"; results.errors.push({ type: "customer_sms_failed", status: customerResult.status, twilio: customerResult.data, }); } else { results.customer_sms = "sent"; } } } if (sendOwnerSms) { if (!ownerPhoneNumber) { results.owner_sms = "skipped_missing_owner_phone"; } else { const ownerMessage = renderTemplate( OWNER_SMS_TEMPLATE, templateValues, ); const ownerResult = await sendTwilioSms({ accountSid, authToken, from: twilioPhoneNumber, to: ownerPhoneNumber, body: ownerMessage, }); if (!ownerResult.ok) { results.owner_sms = "failed"; results.errors.push({ type: "owner_sms_failed", status: ownerResult.status, twilio: ownerResult.data, }); } else { results.owner_sms = "sent"; } } } if (results.errors.length > 0) { return json( { success: false, message: "One or more SMS messages failed.", results, }, 502, ); } return json({ success: true, message: "SMS notification process completed.", results, }); });

ชื่อร้าน

ชื่อร้านของคุณ

ข้อความ SMS ถึงลูกค้า

สวัสดีคุณ {{customer_name}} เราได้รับคำขอจอง {{service_name}} ของคุณสำหรับวันที่ {{appointment_date}} เวลา {{appointment_time}} แล้ว เราจะติดต่อกลับเพื่อยืนยันเร็ว ๆ นี้

ข้อความ SMS ถึงเจ้าของร้าน

คำขอจองใหม่: {{customer_name}} ขอจอง {{service_name}} วันที่ {{appointment_date}} เวลา {{appointment_time}} เบอร์โทร: {{customer_phone}}

ไม่บังคับ: แก้ชื่อฟิลด์ถ้าจำเป็น

ฟังก์ชัน SMS นี้ทำงานกับฟิลด์การจองทั่วไป เช่น: full_name, phone, appointment_date, start_time, service_id และ service_name ถ้าโปรเจกต์ของคุณใช้ชื่อฟิลด์ต่างออกไป ให้แก้ FIELD_MAP ในฟังก์ชันให้ตรงกับตารางการจองของคุณ

FIELD_MAP

const FIELD_MAP = { customerName: ["full_name", "customer_name", "name"], customerPhone: ["phone", "phone_number", "customer_phone"], appointmentDate: ["appointment_date", "date", "booking_date"], appointmentTime: ["start_time", "appointment_time", "time"], serviceName: ["service_name", "service"], serviceId: ["service_id"], };

เชื่อมต่อ Webhook

เลือกตารางการจองของคุณ

ในวิดีโอนี้เราใช้: public.appointments ถ้าโปรเจกต์ของคุณใช้ตารางจองอื่น ให้เลือกตารางนั้นแทน

ไปที่ Supabase Database Webhooks

Supabase → Database → Webhooks → Create webhook

ใช้การตั้งค่า webhook ต่อไปนี้

การตั้งค่า Webhook

Table: public.appointments Event: Insert Type: Supabase Edge Functions Function: send-sms-notification Method: POST

เปิดใช้เฉพาะ event ชื่อ Insert

ทดสอบการแจ้งเตือน SMS

ลองจองใหม่จากเว็บของคุณ

ตรวจสอบว่าลูกค้าได้รับ SMS หรือยัง

ตรวจสอบ Logs ของ Supabase Edge Function

Supabase → Edge Functions → send-sms-notification → Logs

ตรวจสอบ Twilio Messaging Logs ถ้าจำเป็น

Twilio → Messaging → Logs

ก่อนเปิดใช้งานจริง ตรวจสอบกฎเกี่ยวกับ SMS ของประเทศคุณ

ข้อกำหนดเรื่อง SMS แตกต่างกันตามประเทศ

ข้อกำหนดเรื่อง SMS แตกต่างกันไปตามประเทศปลายทาง ก่อนใช้งานจริง ให้ตรวจสอบข้อกำหนดการส่งข้อความของ Twilio สำหรับประเทศเป้าหมายของคุณ เช่น การส่ง SMS จากแอปไปยังเบอร์ในสหรัฐฯ อาจต้องลงทะเบียน A2P 10DLC