Clinic Appointment System

Smart appointment booking and management system for medical clinics

ระบบนัดหมายอัจฉริยะสำหรับคลินิกทันตกรรมและคลินิกแพทย์ในประเทศไทย

Project Overview

Client Background

คลินิกทันตกรรมชั้นนำในกรุงเทพฯ
├─ 3 แพทย์ผู้เชี่ยวชาญ
├─ 5 ห้องตรวจ
├─ 50+ คนไข้ต่อวัน
└─ ปัญหา: การนัดหมายผ่านโทรศัพท์ยุ่งยาก

Challenges Solved

❌ ปัญหาเดิม:
├─ แพทย์ต้องหยุดตรวจรักษามารับโทร
├─ การจดนัดลงสมุดผิดพลาดบ่อย
├─ คนไข้ไม่ทราบเวลาว่างล่าสุด
├─ ไม่มีระบบเตือนความจำ
└─ ประวัติการรักษากระจัดกระจาย

✅ วิธีแก้:
├─ LINE Chatbot รับนัดอัตโนมัติ
├─ Real-time schedule synchronization
├─ Smart time slot recommendations
├─ Automated reminder system
└─ Complete patient management

Technology Stack

Architecture Diagram

LINE OA → n8n (Railway) → Custom API (Vercel) → Supabase Database
    ↓           ↓                    ↓              ↓
Patient    Workflow            Business Logic    Patient Data
    ↓           ↓                    ↓              ↓
Confirmation → Google Calendar ← Dashboard ← Analytics

Components Used

Frontend: Next.js + TailwindCSS (Vercel)
Backend: Node.js/TypeScript (Vercel Functions)
Database: Supabase (PostgreSQL)
Automation: n8n (Railway)
Calendar: Google Calendar API
Authentication: LINE Login + Supabase Auth

Key Features

1. Thai Time Expression Parser

// Thai time expression understanding
const timePatterns = {
  today: ['วันนี้', 'วันนี้เลย', 'วันนี้เอง'],
  tomorrow: ['พรุ่งนี้', 'วันพรุ่งนี้', 'มะรืน'],
  dayAfter: ['วันหลัง', 'มะรืนนี้', '3 วันข้างหน้า'],
  morning: ['เช้า', 'ตอนเช้า', '10 โมงเช้า', '9 โมง'],
  afternoon: ['บ่าย', 'ตอนบ่าย', 'บ่ายๆ', '2 โมงบ่าย'],
  evening: ['เย็น', 'ตอนเย็น', 'หลังเที่ยง', '4 โมงเย็น'],
  urgent: ['ด่วน', 'เร่ง', 'ปวดฟัน', 'ฉุกเฉิน', 'ปวดมาก'],
  checkup: ['ตรวจ', 'เช็ค', 'ตรวจสอบ', 'เช็คประจำปี'],
  cleaning: ['ล้าง', 'ขัด', 'ทำความสะอาด', 'ขัดฟัน'],
  braces: ['จัดฟัน', 'ใส่เหล็ก', 'เหล็ก', 'orthodontic'],
  extraction: ['ถอน', 'ถอนฟัน', 'ฟันคุด', 'ฟันฟก'],
  filling: ['อุด', 'อุดฟัน', 'ฟันผุ', 'สีฟัน']
};

function parseAppointmentRequest(message) {
  const normalizedMessage = message.toLowerCase().trim();
  const parsed = {
    date: null,
    time: null,
    urgency: 'normal',
    serviceType: 'general',
    specialRequests: []
  };
  
  // Parse date
  for (const [period, keywords] of Object.entries(timePatterns)) {
    if (['today', 'tomorrow', 'dayAfter'].includes(period)) {
      const match = keywords.find(keyword => normalizedMessage.includes(keyword));
      if (match) {
        parsed.date = parseDateFromKeyword(period);
        break;
      }
    }
  }
  
  // Parse time of day
  for (const [timeOfDay, keywords] of Object.entries(timePatterns)) {
    if (['morning', 'afternoon', 'evening'].includes(timeOfDay)) {
      const match = keywords.find(keyword => normalizedMessage.includes(keyword));
      if (match) {
        parsed.time = parseTimeFromKeyword(timeOfDay);
        break;
      }
    }
  }
  
  // Extract specific time
  const timeMatch = normalizedMessage.match(/(\d{1,2})[:.]?(\d{2})\s*(โมง|นาฬิกา)?/);
  if (timeMatch) {
    const hour = parseInt(timeMatch[1]);
    const minute = parseInt(timeMatch[2]) || 0;
    parsed.time = { hour, minute };
  }
  
  // Detect urgency
  if (timePatterns.urgent.some(keyword => normalizedMessage.includes(keyword))) {
    parsed.urgency = 'urgent';
  }
  
  // Detect service type
  for (const [service, keywords] of Object.entries(timePatterns)) {
    if (['checkup', 'cleaning', 'braces', 'extraction', 'filling'].includes(service)) {
      const match = keywords.find(keyword => normalizedMessage.includes(keyword));
      if (match) {
        parsed.serviceType = service;
        break;
      }
    }
  }
  
  // Extract special requests
  if (normalizedMessage.includes('แพทย์หญิง')) parsed.specialRequests.push('female_doctor');
  if (normalizedMessage.includes('แพทย์ชาย')) parsed.specialRequests.push('male_doctor');
  if (normalizedMessage.includes('ห้อง vip')) parsed.specialRequests.push('vip_room');
  if (normalizedMessage.includes('ใกล้บ้าน')) parsed.specialRequests.push('nearest_location');
  
  return parsed;
}

function parseDateFromKeyword(keyword) {
  const today = new Date();
  
  switch (keyword) {
    case 'today':
      return today;
    case 'tomorrow':
      const tomorrow = new Date(today);
      tomorrow.setDate(today.getDate() + 1);
      return tomorrow;
    case 'dayAfter':
      const dayAfter = new Date(today);
      dayAfter.setDate(today.getDate() + 2);
      return dayAfter;
    default:
      return today;
  }
}

function parseTimeFromKeyword(timeOfDay) {
  switch (timeOfDay) {
    case 'morning':
      return { hour: 10, minute: 0 };
    case 'afternoon':
      return { hour: 14, minute: 0 };
    case 'evening':
      return { hour: 16, minute: 0 };
    default:
      return { hour: 10, minute: 0 };
  }
}

2. Smart Appointment Booking

// n8n Function Node - Smart Appointment Booking
async function bookClinicAppointment() {
  const bookingData = $json;
  const parsedRequest = parseAppointmentRequest(bookingData.message);
  
  try {
    // Get patient information
    const patient = await getOrCreatePatient(bookingData.userId, bookingData.profile);
    
    // Find available time slots
    const availableSlots = await findAvailableTimeSlots(
      parsedRequest.date || new Date(),
      parsedRequest.serviceType,
      parsedRequest.urgency
    );
    
    if (availableSlots.length === 0) {
      // Find alternatives
      const alternatives = await findAlternativeSlots(parsedRequest);
      return [{
        json: {
          replyToken: bookingData.replyToken,
          response: generateNoSlotsResponse(parsedRequest, alternatives),
          requiresAction: true
        }
      }];
    }
    
    // Select best slot
    const bestSlot = selectBestSlot(availableSlots, parsedRequest);
    
    // Check for conflicts
    const conflicts = await checkConflicts(patient.id, bestSlot);
    if (conflicts.length > 0) {
      return [{
        json: {
          replyToken: bookingData.replyToken,
          response: generateConflictResponse(conflicts),
          requiresAction: true
        }
      }];
    }
    
    // Create appointment
    const appointment = {
      patient_id: patient.id,
      doctor_id: await selectBestDoctor(parsedRequest),
      appointment_date: bestSlot.date,
      appointment_time: bestSlot.time,
      service_type: parsedRequest.serviceType,
      urgency: parsedRequest.urgency,
      status: 'confirmed',
      notes: parsedRequest.specialRequests.join(', '),
      created_at: new Date().toISOString()
    };
    
    // Save to database
    const { data: savedAppointment, error } = await supabase
      .from('appointments')
      .insert(appointment)
      .select(`
        *,
        doctors (name, specialization),
        patients (name, phone)
      `)
      .single();
    
    if (error) throw error;
    
    // Add to Google Calendar
    await addToGoogleCalendar(savedAppointment);
    
    // Schedule reminders
    await scheduleReminders(savedAppointment);
    
    // Send confirmation
    await sendAppointmentConfirmation(bookingData.replyToken, savedAppointment);
    
    return [{ json: savedAppointment }];
    
  } catch (error) {
    console.error('Appointment booking error:', error);
    return [{
      json: {
        replyToken: bookingData.replyToken,
        response: 'ขออภัยครับ เกิดข้อผิดพลาดในการจองนัด กรุณาลองใหม่หรือโทรติดต่อคลินิก',
        error: error.message
      }
    }];
  }
}

async function findAvailableTimeSlots(requestedDate, serviceType, urgency) {
  const clinicHours = {
    weekday: { start: 9, end: 18 },
    saturday: { start: 9, end: 16 },
    sunday: { start: null, end: null } // Closed
  };
  
  const dayOfWeek = requestedDate.getDay();
  const hours = clinicHours[dayOfWeek === 0 ? 'sunday' : dayOfWeek === 6 ? 'saturday' : 'weekday'];
  
  if (!hours.start) return []; // Closed day
  
  const slots = [];
  const slotDuration = getServiceDuration(serviceType);
  
  for (let hour = hours.start; hour < hours.end; hour++) {
    for (let minute = 0; minute < 60; minute += 30) {
      const slotTime = new Date(requestedDate);
      slotTime.setHours(hour, minute, 0, 0);
      
      // Check if slot is available
      const isAvailable = await checkSlotAvailability(slotTime, slotDuration);
      if (isAvailable) {
        slots.push({
          date: slotTime,
          time: { hour, minute },
          duration: slotDuration,
          score: calculateSlotScore(slotTime, urgency)
        });
      }
    }
  }
  
  // Sort by score (best slots first)
  return slots.sort((a, b) => b.score - a.score);
}

async function checkSlotAvailability(slotTime, duration) {
  const endTime = new Date(slotTime.getTime() + duration * 60 * 1000);
  
  // Check for existing appointments
  const { data: conflicts } = await supabase
    .from('appointments')
    .select('*')
    .eq('status', 'confirmed')
    .or(`and(appointment_date.eq.${slotTime.toISOString()},appointment_time.gte.${slotTime.toTimeString().slice(0,5)})`)
    .lt('appointment_time', endTime.toTimeString().slice(0,5));
  
  return conflicts.length === 0;
}

function getServiceDuration(serviceType) {
  const durations = {
    general: 30,
    checkup: 45,
    cleaning: 60,
    braces: 90,
    extraction: 30,
    filling: 45,
    urgent: 30
  };
  
  return durations[serviceType] || durations.general;
}

3. Automated Reminder System

// n8n Workflow - Automated Reminders
async function sendAppointmentReminders() {
  const tomorrow = new Date();
  tomorrow.setDate(tomorrow.getDate() + 1);
  tomorrow.setHours(0, 0, 0, 0);
  
  const oneHourFromNow = new Date();
  oneHourFromNow.setHours(oneHourFromNow.getHours() + 1);
  
  // Get appointments for tomorrow (24-hour reminder)
  const { data: tomorrowAppointments } = await supabase
    .from('appointments')
    .select(`
      *,
      patients (line_user_id, name, phone),
      doctors (name, specialization)
    `)
    .gte('appointment_date', tomorrow.toISOString())
    .lt('appointment_date', new Date(tomorrow.getTime() + 24 * 60 * 60 * 1000).toISOString())
    .eq('status', 'confirmed')
    .eq('reminder_24h_sent', false);
  
  // Get appointments in 1 hour (1-hour reminder)
  const { data: hourAppointments } = await supabase
    .from('appointments')
    .select(`
      *,
      patients (line_user_id, name, phone),
      doctors (name, specialization)
    `)
    .gte('appointment_date', new Date().toISOString())
    .lte('appointment_date', oneHourFromNow.toISOString())
    .eq('status', 'confirmed')
    .eq('reminder_1h_sent', false);
  
  // Send 24-hour reminders
  for (const appointment of tomorrowAppointments) {
    await send24HourReminder(appointment);
    
    // Mark as sent
    await supabase
      .from('appointments')
      .update({ reminder_24h_sent: true })
      .eq('id', appointment.id);
  }
  
  // Send 1-hour reminders
  for (const appointment of hourAppointments) {
    await send1HourReminder(appointment);
    
    // Mark as sent
    await supabase
      .from('appointments')
      .update({ reminder_1h_sent: true })
      .eq('id', appointment.id);
  }
}

async function send24HourReminder(appointment) {
  const message = `
🦷 แจ้งเตือนนัดหมายครับ (พรุ่งนี้)

📅 วันที่: ${formatThaiDate(appointment.appointment_date)}
⏰ เวลา: ${formatThaiTime(appointment.appointment_time)}
👨‍⚕️ แพทย์: คุณหมอ${appointment.doctors.name}
🏥 บริการ: ${getServiceName(appointment.service_type)}

📍 ที่อยู่คลินิก:
123 ถนนสุขุมวิท แขวงคลองตันเหนือ
เขตห้วยขวาง กรุงเทพฯ 10310

📱 โทร: 02-123-4567
🕐 เวลาทำการ: จันทร์-เสาร์ 9:00-18:00 น.

⚠️ ข้อควรทราบ:
• กรุณามาก่อนเวลานัด 15 นาที
• หากต้องการเลื่อน/ยกเลิก แจ้งล่วงหน้า 2 ชั่วโมง
• นำบัตรประชาชนและประวัติการรักษามาด้วย

❓ มีคำถาม? ตอบกลับได้เลยครับ
  `;
  
  await sendLINEMessage(appointment.patients.line_user_id, message);
}

async function send1HourReminder(appointment) {
  const message = `
⏰ แจ้งเตือนนัดหมาย 1 ชั่วโมง!

คุณ ${appointment.patients.name}
นัดกับคุณหมอ${appointment.doctors.name}
เวลา ${formatThaiTime(appointment.appointment_time)}

📍 คลินิกอยู่ใกล้ถึงแล้ว อย่าลืมเดินทางนะครับ!

📞 ถ้าติดขัด โทร: 02-123-4567
  `;
  
  await sendLINEMessage(appointment.patients.line_user_id, message);
}

4. Doctor Dashboard

// Doctor Dashboard Component
export default function DoctorDashboard() {
  const [doctor, setDoctor] = useState(null);
  const [appointments, setAppointments] = useState([]);
  const [selectedDate, setSelectedDate] = useState(new Date());
  
  useEffect(() => {
    // Load doctor profile
    loadDoctorProfile();
    
    // Load appointments for selected date
    loadAppointments(selectedDate);
    
    // Real-time subscription
    const subscription = supabase
      .channel('doctor-appointments')
      .on('postgres_changes',
        { event: '*', schema: 'public', table: 'appointments' },
        (payload) => {
          if (payload.eventType === 'INSERT' || payload.eventType === 'UPDATE') {
            loadAppointments(selectedDate);
          }
        }
      )
      .subscribe();
    
    return () => subscription.unsubscribe();
  }, [selectedDate]);
  
  async function loadDoctorProfile() {
    // Get doctor from authentication
    const { data: { user } } = supabase.auth.getUser();
    if (user) {
      const { data: doctor } = await supabase
        .from('doctors')
        .select('*')
        .eq('user_id', user.id)
        .single();
      
      setDoctor(doctor);
    }
  }
  
  async function loadAppointments(date) {
    if (!doctor) return;
    
    const startOfDay = new Date(date);
    startOfDay.setHours(0, 0, 0, 0);
    
    const endOfDay = new Date(date);
    endOfDay.setHours(23, 59, 59, 999);
    
    const { data: appointments } = await supabase
      .from('appointments')
      .select(`
        *,
        patients (name, phone, date_of_birth, last_visit),
        treatments (name, notes)
      `)
      .eq('doctor_id', doctor.id)
      .gte('appointment_date', startOfDay.toISOString())
      .lte('appointment_date', endOfDay.toISOString())
      .order('appointment_time', { ascending: true });
    
    setAppointments(appointments || []);
  }
  
  return (
    <div className="min-h-screen bg-gray-50 p-6">
      <div className="max-w-7xl mx-auto">
        {/* Header */}
        <div className="bg-white rounded-lg shadow p-6 mb-6">
          <div className="flex justify-between items-center">
            <div>
              <h1 className="text-3xl font-bold">คุณหมอ{doctor?.name}</h1>
              <p className="text-gray-600">{doctor?.specialization}</p>
            </div>
            <div className="flex gap-4">
              <DatePicker 
                selected={selectedDate}
                onChange={setSelectedDate}
                className="px-4 py-2 border border-gray-300 rounded-lg"
              />
              <div className="text-right">
                <div className="text-2xl font-bold text-blue-600">
                  {appointments.length}
                </div>
                <div className="text-sm text-gray-600">คนไข้วันนี้</div>
              </div>
            </div>
          </div>
        </div>
        
        {/* Appointments Timeline */}
        <div className="bg-white rounded-lg shadow p-6">
          <h2 className="text-xl font-bold mb-4">ตารางนัดหมาย</h2>
          
          {appointments.length === 0 ? (
            <div className="text-center py-12 text-gray-500">
              ไม่มีนัดหมายในวันนี้
            </div>
          ) : (
            <div className="space-y-4">
              {appointments.map(appointment => (
                <AppointmentCard 
                  key={appointment.id} 
                  appointment={appointment}
                  onUpdateStatus={updateAppointmentStatus}
                />
              ))}
            </div>
          )}
        </div>
        
        {/* Quick Stats */}
        <div className="grid grid-cols-4 gap-6 mt-6">
          <StatCard title="คนไข้วันนี้" value={appointments.length} />
          <StatCard title="คนไข้สะสม" value="1,234" />
          <StatCard title="นัดหมายสัปดาห์นี้" value="45" />
          <StatCard title="รายได้เดือนนี้" value="฿234,500" />
        </div>
      </div>
    </div>
  );
}

function AppointmentCard({ appointment, onUpdateStatus }) {
  const [isExpanded, setIsExpanded] = useState(false);
  
  const statusColors = {
    confirmed: 'bg-blue-100 text-blue-800',
    in_progress: 'bg-yellow-100 text-yellow-800',
    completed: 'bg-green-100 text-green-800',
    cancelled: 'bg-red-100 text-red-800',
    no_show: 'bg-gray-100 text-gray-800'
  };
  
  const urgencyColors = {
    normal: 'text-gray-600',
    urgent: 'text-red-600 font-bold'
  };
  
  return (
    <div className="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow">
      <div className="flex justify-between items-start">
        <div className="flex-1">
          <div className="flex items-center gap-3 mb-2">
            <span className="text-lg font-bold">
              {formatThaiTime(appointment.appointment_time)}
            </span>
            <span className={`px-2 py-1 rounded-full text-xs font-medium ${statusColors[appointment.status]}`}>
              {getStatusText(appointment.status)}
            </span>
            <span className={urgencyColors[appointment.urgency]}>
              {appointment.urgency === 'urgent' ? '🔴 ด่วน' : ''}
            </span>
          </div>
          
          <div className="mb-2">
            <span className="font-medium">คนไข้: </span>
            {appointment.patients.name}
            <span className="text-gray-500 ml-2">
              ({appointment.patients.phone})
            </span>
          </div>
          
          <div className="text-sm text-gray-600">
            <span className="font-medium">บริการ: </span>
            {getServiceName(appointment.service_type)}
            {appointment.notes && (
              <span className="ml-2">({appointment.notes})</span>
            )}
          </div>
          
          {isExpanded && (
            <div className="mt-4 pt-4 border-t border-gray-200">
              <div className="grid grid-cols-2 gap-4 text-sm">
                <div>
                  <span className="font-medium">วันเกิด: </span>
                  {formatThaiDate(appointment.patients.date_of_birth)}
                </div>
                <div>
                  <span className="font-medium">รับการรักษาครั้งล่าสุด: </span>
                  {formatThaiDate(appointment.patients.last_visit)}
                </div>
              </div>
              
              {appointment.treatments && (
                <div className="mt-3">
                  <span className="font-medium">การรักษาครั้งล่าสุด: </span>
                  <div className="text-sm text-gray-600 mt-1">
                    {appointment.treatments.name}
                    {appointment.treatments.notes && (
                      <div>{appointment.treatments.notes}</div>
                    )}
                  </div>
                </div>
              )}
            </div>
          )}
        </div>
        
        <div className="flex flex-col gap-2">
          <button
            onClick={() => setIsExpanded(!isExpanded)}
            className="text-blue-600 text-sm hover:underline"
          >
            {isExpanded ? 'ซ่อน' : 'ดูเพิ่มเติม'}
          </button>
          
          <div className="flex gap-2">
            <button
              onClick={() => onUpdateStatus(appointment.id, 'in_progress')}
              className="px-3 py-1 bg-yellow-500 text-white rounded text-sm"
              disabled={appointment.status !== 'confirmed'}
            >
              เริ่มตรวจ
            </button>
            <button
              onClick={() => onUpdateStatus(appointment.id, 'completed')}
              className="px-3 py-1 bg-green-500 text-white rounded text-sm"
              disabled={appointment.status !== 'in_progress'}
            >
              เสร็จสิ้น
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

5. Patient Mobile App (LINE LIFF)

// Patient LIFF Application
export default function PatientLIFF() {
  const [user, setUser] = useState(null);
  const [appointments, setAppointments] = useState([]);
  const [activeTab, setActiveTab] = useState('upcoming');
  
  useEffect(() => {
    // Initialize LIFF
    liff.init({ liffId: process.env.NEXT_PUBLIC_LIFF_ID })
      .then(() => {
        if (liff.isLoggedIn()) {
          const profile = liff.getDecodedIDToken();
          setUser(profile);
          loadPatientData(profile.sub);
        } else {
          liff.login();
        }
      });
  }, []);
  
  async function loadPatientData(userId) {
    try {
      // Get patient information
      const { data: patient } = await supabase
        .from('patients')
        .select('*')
        .eq('line_user_id', userId)
        .single();
      
      if (patient) {
        // Load appointments
        const { data: appointments } = await supabase
          .from('appointments')
          .select(`
            *,
            doctors (name, specialization),
            treatments (name, notes)
          `)
          .eq('patient_id', patient.id)
          .order('appointment_date', { ascending: true })
          .limit(10);
        
        setAppointments(appointments || []);
      }
    } catch (error) {
      console.error('Load patient data error:', error);
    }
  }
  
  const upcomingAppointments = appointments.filter(apt => 
    new Date(apt.appointment_date) > new Date() && apt.status === 'confirmed'
  );
  
  const pastAppointments = appointments.filter(apt => 
    new Date(apt.appointment_date) <= new Date() || 
    ['completed', 'cancelled', 'no_show'].includes(apt.status)
  );
  
  return (
    <div className="min-h-screen bg-gray-50">
      {/* Header */}
      <div className="bg-green-600 text-white p-4">
        <div className="flex items-center gap-3">
          <img 
            src={user?.picture} 
            alt={user?.name}
            className="w-12 h-12 rounded-full border-2 border-white"
          />
          <div>
            <h1 className="text-xl font-bold">{user?.name}</h1>
            <p className="text-green-100">คลินิกทันตกรรม ShantiLink</p>
          </div>
        </div>
      </div>
      
      {/* Quick Actions */}
      <div className="bg-white p-4 shadow-sm">
        <div className="grid grid-cols-2 gap-3">
          <button className="bg-blue-500 text-white p-3 rounded-lg font-medium">
            📅 จองนัดหมาย
          </button>
          <button className="bg-orange-500 text-white p-3 rounded-lg font-medium">
            📞 ติดต่อคลินิก
          </button>
        </div>
      </div>
      
      {/* Tabs */}
      <div className="bg-white border-b">
        <div className="flex">
          <button
            onClick={() => setActiveTab('upcoming')}
            className={`flex-1 py-3 font-medium ${
              activeTab === 'upcoming' 
                ? 'text-blue-600 border-b-2 border-blue-600' 
                : 'text-gray-600'
            }`}
          >
            นัดหมายที่จะถึง
          </button>
          <button
            onClick={() => setActiveTab('past')}
            className={`flex-1 py-3 font-medium ${
              activeTab === 'past' 
                ? 'text-blue-600 border-b-2 border-blue-600' 
                : 'text-gray-600'
            }`}
          >
            ประวัติการรักษา
          </button>
        </div>
      </div>
      
      {/* Content */}
      <div className="p-4">
        {activeTab === 'upcoming' ? (
          <UpcomingAppointments appointments={upcomingAppointments} />
        ) : (
          <PastAppointments appointments={pastAppointments} />
        )}
      </div>
    </div>
  );
}

function UpcomingAppointments({ appointments }) {
  if (appointments.length === 0) {
    return (
      <div className="text-center py-12 text-gray-500">
        <div className="text-6xl mb-4">📅</div>
        <p>ยังไม่มีนัดหมายที่จะถึง</p>
        <button className="mt-4 bg-blue-500 text-white px-6 py-2 rounded-lg">
          จองนัดหมายใหม่
        </button>
      </div>
    );
  }
  
  return (
    <div className="space-y-4">
      {appointments.map(appointment => (
        <div key={appointment.id} className="bg-white rounded-lg shadow p-4">
          <div className="flex justify-between items-start mb-3">
            <div>
              <div className="font-bold text-lg">
                {formatThaiDate(appointment.appointment_date)}
              </div>
              <div className="text-gray-600">
                {formatThaiTime(appointment.appointment_time)}
              </div>
            </div>
            <span className="bg-green-100 text-green-800 px-2 py-1 rounded-full text-xs font-medium">
              ยืนยันแล้ว
            </span>
          </div>
          
          <div className="mb-3">
            <div className="font-medium">คุณหมอ{appointment.doctors.name}</div>
            <div className="text-sm text-gray-600">
              {appointment.doctors.specialization}
            </div>
          </div>
          
          <div className="text-sm text-gray-600 mb-3">
            บริการ: {getServiceName(appointment.service_type)}
          </div>
          
          <div className="flex gap-2">
            <button className="flex-1 bg-blue-500 text-white py-2 rounded text-sm">
              เลื่อนนัด
            </button>
            <button className="flex-1 bg-red-500 text-white py-2 rounded text-sm">
              ยกเลิกนัด
            </button>
          </div>
        </div>
      ))}
    </div>
  );
}

Implementation Timeline

Phase 1: Foundation (Week 1-2)

✅ เสร็จสิ้น:
├─ Setup Supabase database schema
├─ Create n8n appointment workflow
├─ Implement LINE webhook integration
├─ Thai time expression parser
└─ Basic appointment booking

Phase 2: Core Features (Week 3-4)

✅ เสร็จสิ้น:
├─ Smart slot recommendation
├─ Google Calendar integration
├─ Automated reminder system
├─ Doctor dashboard
└─ Patient LIFF application

Phase 3: Advanced Features (Week 5-6)

✅ เสร็จสิ้น:
├─ Medical record management
├─ Treatment history tracking
├─ Analytics and reporting
├─ Multi-clinic support
└─ Payment processing

Results & Metrics

Before vs After

📊 ผลลัพธ์หลัง 2 เดือน:

Appointment Management:
├─ No-show rate: 25% ↓ 10%
├─ Booking errors: 90% ↓ 5%
├─ Reminder effectiveness: 0% ↑ 95%
└─ Staff time saved: 15 ชั่วโมง/สัปดาห์

Patient Experience:
├─ Satisfaction score: 70% ↑ 95%
├─ Booking convenience: 40% ↑ 90%
├─ Communication clarity: 60% ↑ 95%
└─ Trust in clinic: +40%

Patient Feedback

💬 ความคิดเห็นคนไข้:
├─ "จองนัดง่ายมาก ไม่ต้องโทรรอ"
├─ "มีเตือนก่อนนัด ไม่ลืมแล้ว"
├─ "รู้เวลาว่างทันที สะดวกมาก"
├─ "แอปใช้ง่าย ดูประวัติได้"
└─ "รู้สึกใส่ใจลูกค้ามากขึ้น"

Doctor Feedback

👨‍⚕️ ความคิดเห็นแพทย์:
├─ "ไม่ต้องหยุดตรวจรับโทรแล้ว"
├─ "ตารางเป็นระเบียบขึ้นเยอะ"
├─ "รู้ประวัติคนไข้ก่อนตรวจ"
├─ "เตรียมตัวได้ล่วงหน้า"
└─ "มีเวลาให้คนไข้มากขึ้น"

Cost Analysis

Investment Breakdown

💰 การลงทุน (ครั้งเดียว):
├─ System development: ฿35,000
├─ LIFF application: ฿15,000
├─ Integration setup: ฿10,000
├─ Training & documentation: ฿10,000
└─ Total: ฿70,000

💸 ค่าใช้จ่ายรายเดือน:
├─ n8n hosting: ฿400
├─ Database: ฿600
├─ Frontend: ฿200
├─ LINE OA: ฿1,500
├─ Google Calendar API: ฿100
└─ Total: ฿2,800/เดือน

ROI Calculation

📈 Return on Investment:
├─ Reduced no-show revenue: ฿30,000/เดือน
├─ Staff time savings: ฿25,000/เดือน
├─ Increased patient capacity: ฿40,000/เดือน
├─ Net monthly gain: ฿92,200
├─ Payback period: 0.8 เดือน
└─ Annual ROI: 1,579%

Technical Challenges & Solutions

Challenge 1: Thai Time Understanding

❌ ปัญหา:
├─ "พรุ่งนี้บ่ายๆ" ไม่ชัดเจน
├─ "ชายๆ 2 โมง" ต้องการความเร่งด่วน
├─ "อาทิตย์หน้า" ต้องคำนวณ
└─ การใช้ศัพท์แต่ละคนไม่เหมือนกัน

✅ วิธีแก้:
├─ สร้าง pattern matching ซับซ้อน
├─ ใช้ AI ช่วย interpret ความหมาย
├─ ยืนยันกับคนไข้ทุกครั้ง
└─ เรียนรู้จากข้อมูลจริง

Challenge 2: Real-time Calendar Sync

❌ ปัญหา:
├─ การเปลี่ยนแปลงตารางแพทย์
├─ การจองซ้ำในเวลาเดียวกัน
├─ การยกเลิกนัดฉุกเฉิน
└─ การส่งมอบคนไข้ระหว่างแพทย์

✅ วิธีแก้:
├─ Real-time subscriptions
├─ Database transactions
├─ Conflict detection algorithms
└─ Automated notifications

Lessons Learned

Technical Lessons

✅ Do's:
├─ ใช้ Thai NLP ที่เข้าใจบริบท
├─ สร้าง confirmation loop ทุกขั้นตอน
├─ ใช้ real-time อย่างมีประสิทธิภาพ
├─ วางแผนสำรองข้อมูลดีๆ
└─ Test กับผู้ใช้จริงตั้งแต่แรก

❌ Don'ts:
├─ สมมติว่าคนไข้เข้าใจระบบ
├─ ลืม emergency cases
├─ ไม่มี backup communication
├─ ใช้ time zones ผิด
└─ ไม่คิดถึง data privacy

Business Lessons

✅ Success Factors:
├─ เข้าใจ workflow ของคลินิกจริง
├─ สร้าง trust กับคนไข้
├─ ลดภาระงานพนักงาน
├─ วัดผลและปรับปรุง
└─ สนับสนุนการเปลี่ยนแปลง

Future Enhancements

Phase 4: Advanced Features (Planned)

🚀 คุณสมบัติที่จะเพิ่ม:
├─ AI-powered treatment recommendations
├─ Telemedicine integration
├─ Insurance processing
├─ Advanced medical imaging
├─ Multi-specialty clinic support
└─ Patient education content

ต้องระบบนัดหมายแบบนี้หรือไม่? ติดต่อเราได้ที่ ShantiLink.com 💬

ดูตัวอย่างอื่นๆ: