# Booking Page - Architecture Diagram ## ๐Ÿ›๏ธ Complete System Architecture ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ WECHAT MINI-PROGRAM โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ FRONTEND (Vue 3 + Uni-app) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ pages/booking/index.vue (Main Page Component) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ State: โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข selectedDate: string โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข selectedPeriod: PeriodKey | null โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข showConfirmPopup: boolean โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข pendingSlot: TimeSlotWithBookingStatus | null โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข refreshing: boolean โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Computed: โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข scrollHeight (responsive) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข filteredSlots (depends on period) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Lifecycle: โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข onMounted() โ†’ Load memberships + today's slots โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Event Handlers: โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข onDateSelect() โ†’ loadSlots(newDate) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข onPeriodChange() โ†’ Auto-filter via computed โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข onRefresh() โ†’ Reload slots โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข onBookTap() โ†’ Auth check โ†’ Show popup โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข onConfirmBooking() โ†’ Create booking โ†’ Refresh โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข onCancelTap() โ†’ Cancel booking โ†’ Refresh โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ†“ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Child Components (All reactive & event-driven) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ DateSelector.vue โ”‚ โ”‚ TimePeriod...vue โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ [Today] [5] [4] โ”‚ โ”‚ ๅ…จ้ƒจ ไธŠๅˆ ไธ‹ๅˆ... โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Props: modelValueโ”‚ โ”‚ Props: modelValueโ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Emit: @select โ”‚ โ”‚ Emit: @change โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ†“ โ†“ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ (Updates selectedDate) (Updates selectedPeriod) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ†“ โ†“ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ (Triggers loadSlots) (Recomputes filteredSlots) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ SlotCard.vue (Rendered via v-for over filteredSlots) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ [09:00-10:00] [0/1 ไบบ] โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ [ๅฏ้ข„็บฆ] โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Props: slot (TimeSlotWithBookingStatus) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Emit: @book | @cancel โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Computed: โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข capacityLabel ("0/1 ไบบ" | "ๅทฒๅ…ณ้—ญ") โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข capacityClass (cap-open | cap-almost | ...) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Button States (4 conditions): โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 1. OPEN + not booked โ†’ "ๅฏ้ข„็บฆ" โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 2. OPEN + booked โ†’ "ๅทฒ้ข„็บฆ" + "ๅ–ๆถˆ" โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 3. FULL โ†’ "ๅทฒ็บฆๆปก" โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 4. CLOSED โ†’ "ๅทฒๅ…ณ้—ญ" โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ†“ โ†“ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ (onBookTap) (onCancelTap) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ BookingConfirmPopup.vue (Modal) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Props: โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข visible: boolean โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข slot: TimeSlotWithBookingStatus โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข memberships: MembershipWithCardType[] โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ State: โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข selectedMembershipId (auto-selected on show) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Display: โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ ็กฎ่ฎค้ข„็บฆ โœ• โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ ๆ—ฅๆœŸ: 2026-04-05 โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ ๆ—ถ้—ด: 09:00 - 10:00 โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ ๅ‰ฉไฝ™: 1 ไธชๅ้ข โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ ๐Ÿ’ณ ็งๆ•™่ฏพ็จ‹ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ ๅ‰ฉไฝ™ 10 ๆฌก โœ“ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ [ๅ–ๆถˆ] [็กฎ่ฎค้ข„็บฆ] โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ†“ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Emit: @confirm({timeSlotId, membershipId}) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ or @cancel โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ†“ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Pinia Stores (Reactive State Management) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ stores/booking.ts: โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ State: โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข slots: TimeSlotWithBookingStatus[] โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข myBookings: BookingWithDetails[] โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข upcomingBookings: BookingWithDetails[] โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข loadingSlots: boolean โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข loadingBookings: boolean โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Actions: โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข fetchSlots(date) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข createBooking(dto) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข cancelBooking(bookingId) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข fetchMyBookings() โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข fetchUpcomingBookings() โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ stores/user.ts: โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ State: โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข user: UserProfileResponse | null โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข memberships: MembershipWithCardType[] โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข token: string (from localStorage) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Computed: โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข loggedIn: !!token โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข hasValidMembership: activeMemberships.length > 0 โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข activeMemberships: memberships filtered by ACTIVE โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Actions: โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข login() โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข fetchMemberships() โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข fetchProfile() โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข logout() โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ†“ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Utils & Helpers โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ utils/request.ts (HTTP Client): โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข request(options): Promise โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข get(url, data?): Promise โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข post(url, data?): Promise โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข put(url, data?): Promise โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ utils/format.ts (Date Utilities): โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข formatDate(date): string โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข getWeekdayLabel(date): string โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข isToday(date): boolean โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข getDateRange(days): DateInfo[] โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ†• โ”‚ โ”‚ HTTP Requests โ”‚ โ”‚ (Bearer Token in Header) โ”‚ โ”‚ โ†“ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ BACKEND API โ”‚ โ”‚ โ”‚ โ”‚ (packages/server/src/time-slot, booking, membership modules) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ GET /api/time-slot/available?date=YYYY-MM-DD โ”‚ โ”‚ โ”‚ โ”‚ โ†’ TimeSlotWithBookingStatus[] โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ POST /api/booking โ”‚ โ”‚ โ”‚ โ”‚ Body: { timeSlotId, membershipId } โ”‚ โ”‚ โ”‚ โ”‚ โ†’ BookingWithDetails โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ PUT /api/booking/:bookingId/cancel โ”‚ โ”‚ โ”‚ โ”‚ โ†’ BookingWithDetails (status: CANCELLED) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ GET /api/membership/my โ”‚ โ”‚ โ”‚ โ”‚ โ†’ MembershipWithCardType[] โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ†• โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ DATABASE โ”‚ โ”‚ โ”‚ โ”‚ (TimeSlot, Booking, Membership, User tables) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` --- ## ๐Ÿ“Š Data Flow Lifecycle ``` โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— โ•‘ BOOKING PAGE LIFECYCLE โ•‘ โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 1. PAGE LOAD โ”Œโ”€ onMounted() โ”‚ โ”œโ”€ IF loggedIn AND no memberships โ”‚ โ”‚ โ””โ”€ userStore.fetchMemberships() โ”‚ โ”‚ GET /membership/my โ”‚ โ”‚ โ†’ memberships array โ”‚ โ”‚ โ”‚ โ””โ”€ loadSlots(today) โ”‚ โ†’ bookingStore.fetchSlots(date) โ”‚ GET /time-slot/available?date=YYYY-MM-DD โ”‚ โ†’ slots array โ”‚ โ†’ Render SlotCard components โ”‚ โ””โ”€ READY โœ“ 2. USER SELECTS DATE โ”œโ”€ onDateSelect(newDate) โ”œโ”€ selectedDate.value = newDate โ””โ”€ loadSlots(newDate) โ†’ bookingStore.fetchSlots(newDate) โ†’ slots array (for new date) โ†’ SlotCard components re-render 3. USER SELECTS TIME PERIOD โ”œโ”€ onPeriodChange(period) โ”œโ”€ selectedPeriod.value = period โ””โ”€ filteredSlots computed updates automatically โ†’ Vue watches TIME_PERIODS[period] โ†’ Filters slots by startTime โ†’ SlotCard components re-render (subset) 4. USER PULLS TO REFRESH โ”œโ”€ onRefresh() โ”œโ”€ refreshing.value = true โ”œโ”€ loadSlots(selectedDate.value) โ”‚ โ†’ bookingStore.fetchSlots() โ”‚ โ†’ slots array (refreshed) โ””โ”€ refreshing.value = false 5. USER TAPS "ๅฏ้ข„็บฆ" (Book) โ”œโ”€ onBookTap(slot) โ”‚ โ”œโ”€ CHECK: loggedIn? โ”‚ โ”œโ”€ NO โ†’ Show login modal โ”‚ โ”‚ User clicks confirm โ”‚ โ”‚ โ†’ userStore.login() โ”‚ โ”‚ POST /auth/wxLogin โ”‚ โ”‚ โ†’ token + user โ”‚ โ”‚ โ†’ userStore.fetchMemberships() โ”‚ โ”‚ GET /membership/my โ”‚ โ”‚ โ†’ memberships array โ”‚ โ”‚ โ†’ RETRY onBookTap(slot) โ”‚ โ”‚ โ”‚ โ””โ”€ YES โ†’ Continue โ”‚ โ”œโ”€ CHECK: hasValidMembership? โ”‚ โ”œโ”€ NO โ†’ Show purchase modal โ”‚ โ”‚ User clicks confirm โ”‚ โ”‚ โ†’ uni.navigateTo('/pages/store/index') โ”‚ โ”‚ โ”‚ โ””โ”€ YES โ†’ Continue โ”‚ โ”œโ”€ pendingSlot.value = slot โ”œโ”€ showConfirmPopup.value = true โ”‚ โ””โ”€ POPUP SHOWN โœ“ โ”œโ”€ selectedMembershipId auto-selected (first one) โ”œโ”€ Watch on popup visibility + memberships โ”‚ โ†’ Auto-select first membership when shown โ”‚ โ””โ”€ User sees: โ€ข Slot date/time โ€ข Membership card options โ€ข Deduction message 6. USER CONFIRMS BOOKING โ”œโ”€ onConfirmBooking({timeSlotId, membershipId}) โ”œโ”€ showConfirmPopup.value = false โ”œโ”€ uni.showLoading('้ข„็บฆไธญ...') โ”‚ โ”œโ”€ bookingStore.createBooking(payload) โ”‚ โ””โ”€ POST /booking โ”‚ Body: { timeSlotId, membershipId } โ”‚ โ†’ BookingWithDetails โ”‚ โ”œโ”€ uni.hideLoading() โ”œโ”€ uni.showToast('้ข„็บฆๆˆๅŠŸ๏ผ') โ”‚ โ”œโ”€ loadSlots(selectedDate.value) // REFRESH โ”‚ โ†’ bookingStore.fetchSlots() โ”‚ GET /time-slot/available?date= โ”‚ โ†’ slots array (UPDATED) โ”‚ โ€ข slot.isBookedByMe = true โ”‚ โ€ข slot.myBookingId = bookingId โ”‚ โ€ข Button now shows "ๅทฒ้ข„็บฆ" โ”‚ โ””โ”€ BOOKING COMPLETE โœ“ 7. USER TAPS "ๅ–ๆถˆ" (Cancel) โ”œโ”€ onCancelTap(slot) โ”œโ”€ Show confirmation modal โ”œโ”€ User confirms โ”‚ โ”œโ”€ uni.showLoading('ๅ–ๆถˆไธญ...') โ”‚ โ”œโ”€ bookingStore.cancelBooking(slot.myBookingId) โ”‚ โ””โ”€ PUT /booking/:id/cancel โ”‚ โ†’ BookingWithDetails (status: CANCELLED) โ”‚ โ”œโ”€ uni.hideLoading() โ”œโ”€ uni.showToast('ๅทฒๅ–ๆถˆ้ข„็บฆ') โ”‚ โ”œโ”€ loadSlots(selectedDate.value) // REFRESH โ”‚ โ†’ bookingStore.fetchSlots() โ”‚ GET /time-slot/available?date= โ”‚ โ†’ slots array (UPDATED) โ”‚ โ€ข slot.isBookedByMe = false โ”‚ โ€ข slot.myBookingId = null โ”‚ โ€ข Button now shows "ๅฏ้ข„็บฆ" โ”‚ โ””โ”€ CANCELLATION COMPLETE โœ“ ``` --- ## ๐Ÿ”„ State Synchronization ``` Component โ†โ†’ Pinia Store โ†โ†’ API โ†โ†’ Database โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Component (Vue Template) โ”‚ โ”‚ โ”‚ โ”‚ {{ bookingStore.slots }} โ† Reactive binding โ”‚ โ”‚ {{ filteredSlots }} โ† Computed from slots โ”‚ โ”‚ {{ userStore.hasValidMembership }} โ† Computed from store โ”‚ โ”‚ โ”‚ โ”‚ @click="onBookTap(slot)" โ† User action โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ†‘ โ†“ โ”‚ Read โ”‚ Mutate โ”‚ โ†“ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Pinia Store State โ”‚ โ”‚ โ”‚ โ”‚ slots: TimeSlotWithBookingStatus[] โ”‚ โ”‚ โ†“ Recomputed when: โ”‚ โ”‚ - fetchSlots() returns data โ”‚ โ”‚ - createBooking() succeeds โ”‚ โ”‚ - cancelBooking() succeeds โ”‚ โ”‚ โ”‚ โ”‚ memberships: MembershipWithCardType[] โ”‚ โ”‚ โ†“ Set when: โ”‚ โ”‚ - fetchMemberships() returns data โ”‚ โ”‚ โ”‚ โ”‚ loadingSlots: boolean โ”‚ โ”‚ โ†“ Set to: โ”‚ โ”‚ - true on fetchSlots() start โ”‚ โ”‚ - false on fetchSlots() end โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ†‘ โ†“ โ”‚ Response โ”‚ Request โ”‚ โ†“ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ API Layer (utils/request.ts) โ”‚ โ”‚ โ”‚ โ”‚ GET /time-slot/available?date=2026-04-05 โ”‚ โ”‚ โ†“ Returns ApiResponse โ”‚ โ”‚ { success: true, data: [...], message: null } โ”‚ โ”‚ โ”‚ โ”‚ POST /booking โ”‚ โ”‚ โ†“ Body: { timeSlotId, membershipId } โ”‚ โ”‚ โ†“ Returns ApiResponse โ”‚ โ”‚ { success: true, data: {...}, message: null } โ”‚ โ”‚ โ”‚ โ”‚ PUT /booking/:id/cancel โ”‚ โ”‚ โ†“ Returns ApiResponse โ”‚ โ”‚ { success: true, data: {...}, message: null } โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ†‘ โ†“ โ”‚ SELECT/UPDATE โ”‚ INSERT/UPDATE โ”‚ โ†“ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Database โ”‚ โ”‚ โ”‚ โ”‚ TimeSlot Table โ”‚ โ”‚ id, date, startTime, endTime, capacity, โ”‚ โ”‚ bookedCount, status, source, templateId โ”‚ โ”‚ โ”‚ โ”‚ Booking Table โ”‚ โ”‚ id, userId, timeSlotId, membershipId, โ”‚ โ”‚ status (CONFIRMED/CANCELLED/...), bookedAt โ”‚ โ”‚ โ”‚ โ”‚ Membership Table โ”‚ โ”‚ id, userId, cardTypeId, status, remainingTimes, โ”‚ โ”‚ expireDate, createdAt, updatedAt โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` --- ## ๐ŸŽฏ Component Communication ``` Root: pages/booking/index.vue โ”‚ โ”œโ”€ PROPS DOWN โ”€โ”€โ†’ DateSelector.vue โ”‚ โ””โ”€ modelValue: string (YYYY-MM-DD) โ”‚ โ”œโ”€ PROPS DOWN โ”€โ”€โ†’ TimePeriodFilter.vue โ”‚ โ””โ”€ modelValue: PeriodKey | null โ”‚ โ”œโ”€ PROPS DOWN โ”€โ”€โ†’ SlotCard.vue (v-for) โ”‚ โ””โ”€ slot: TimeSlotWithBookingStatus โ”‚ โ”œโ”€ PROPS DOWN โ”€โ”€โ†’ BookingConfirmPopup.vue โ”‚ โ”œโ”€ visible: boolean โ”‚ โ”œโ”€ slot: TimeSlotWithBookingStatus | null โ”‚ โ””โ”€ memberships: MembershipWithCardType[] โ”‚ โ”œโ”€ EVENTS UP โ†โ”€โ”€ DateSelector.vue โ”‚ โ”œโ”€ @select(date) โ†’ onDateSelect() โ”‚ โ””โ”€ @update:modelValue(date) โ”‚ โ”œโ”€ EVENTS UP โ†โ”€โ”€ TimePeriodFilter.vue โ”‚ โ”œโ”€ @change(period) โ†’ onPeriodChange() โ”‚ โ””โ”€ @update:modelValue(period) โ”‚ โ”œโ”€ EVENTS UP โ†โ”€โ”€ SlotCard.vue โ”‚ โ”œโ”€ @book(slot) โ†’ onBookTap() โ”‚ โ””โ”€ @cancel(slot) โ†’ onCancelTap() โ”‚ โ””โ”€ EVENTS UP โ†โ”€โ”€ BookingConfirmPopup.vue โ”œโ”€ @confirm({timeSlotId, membershipId}) โ†’ onConfirmBooking() โ””โ”€ @cancel โ†’ showConfirmPopup = false ``` --- ## ๐Ÿงฌ Reactive Dependency Chain ``` LocalStorage (token) โ†“ userStore.token โ†“ userStore.loggedIn (computed) โ†“ pages/booking โ†’ Check login status โ†“ userStore.memberships โ†“ userStore.activeMemberships (computed, filtered by ACTIVE) โ†“ userStore.hasValidMembership (computed) โ†“ pages/booking โ†’ Show/hide booking button & membership popup โ†“ BookingConfirmPopup โ† receives activeMemberships as props โ†“ selectedMembershipId (auto-selected on popup show) bookingStore.slots (array) โ†“ pages/booking.selectedPeriod โ†“ pages/booking.filteredSlots (computed, filtered by TIME_PERIODS) โ†“ v-for โ†’ SlotCard components render โ†“ Each SlotCard โ†’ capacityLabel (computed) โ†’ capacityClass (computed) โ†’ Button state determined bookingStore.loadingSlots (boolean) โ†“ pages/booking template โ†“ v-if โ†’ Show skeleton | Show slots | Show empty state ``` --- ## ๐Ÿ“‹ API Request/Response Chain ``` USER TAPS DATE โ†“ pages/booking/onDateSelect() โ†“ loadSlots(date) โ†“ bookingStore.fetchSlots(date) โ†“ get('/time-slot/available', { date }) โ†“ utils/request.get() โ†“ uni.request({ url: 'http://localhost:3000/api/time-slot/available', method: 'GET', data: { date: '2026-04-05' }, header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' } }) โ†“ BACKEND: GET /api/time-slot/available?date=2026-04-05 (Queries database for TimeSlot records matching date) (Fetches current user's bookings for those slots) (Enriches response with isBookedByMe, myBookingId) โ†“ Response: { "success": true, "data": [ { "id": "...", "date": "2026-04-05", "startTime": "09:00", "endTime": "10:00", "capacity": 1, "bookedCount": 0, "status": "OPEN", "source": "MANUAL", "templateId": null, "isBookedByMe": false, "myBookingId": null }, ... ], "message": null } โ†“ request.ts success callback โ”œโ”€ Check: statusCode < 400 โœ“ โ”œโ”€ Check: body.success === true โœ“ โ”œโ”€ Extract: body.data (TimeSlotWithBookingStatus[]) โ””โ”€ Resolve promise with data โ†“ bookingStore.fetchSlots() try block โ”œโ”€ slots.value = data โ””โ”€ loadingSlots.value = false โ†“ Component template reactivity โ”œโ”€ Re-render with new slots โ”œโ”€ Compute filteredSlots โ””โ”€ Render SlotCard components ```