# Component & Data Flow Hierarchy ## ๐Ÿ—๏ธ Component Tree ``` pages/booking/index.vue (Main Page) โ”‚ โ”œโ”€โ”€ DateSelector.vue โ”‚ โ””โ”€โ”€ Emits: @select (date string) โ”‚ Props: v-model (current date) โ”‚ โ”œโ”€โ”€ TimePeriodFilter.vue โ”‚ โ””โ”€โ”€ Emits: @change (period key) โ”‚ Props: v-model (current period) โ”‚ โ”œโ”€โ”€ SlotCard.vue (Multiple, v-for) โ”‚ โ”œโ”€โ”€ Props: slot (TimeSlotWithBookingStatus) โ”‚ โ”œโ”€โ”€ Emits: @book (slot) / @cancel (slot) โ”‚ โ””โ”€โ”€ Computed: capacityLabel, capacityClass โ”‚ โ””โ”€โ”€ BookingConfirmPopup.vue (Modal) โ”œโ”€โ”€ Props: visible, slot, memberships โ”œโ”€โ”€ Emits: @confirm ({timeSlotId, membershipId}) โ”œโ”€โ”€ Emits: @cancel โ””โ”€โ”€ State: selectedMembershipId ``` --- ## ๐Ÿ”„ State Management Flow ``` Pinia Store (stores/booking.ts) โ”œโ”€โ”€ State: โ”‚ โ”œโ”€โ”€ slots: TimeSlotWithBookingStatus[] โ”‚ โ”œโ”€โ”€ myBookings: BookingWithDetails[] โ”‚ โ”œโ”€โ”€ upcomingBookings: BookingWithDetails[] โ”‚ โ”œโ”€โ”€ loadingSlots: boolean โ”‚ โ””โ”€โ”€ loadingBookings: boolean โ”‚ โ””โ”€โ”€ Actions: โ”œโ”€โ”€ fetchSlots(date) โ†’ GET /time-slot/available?date= โ”œโ”€โ”€ createBooking({...}) โ†’ POST /booking โ”œโ”€โ”€ cancelBooking(bookingId) โ†’ PUT /booking/:id/cancel โ”œโ”€โ”€ fetchMyBookings(status?) โ†’ GET /booking/my โ””โ”€โ”€ fetchUpcomingBookings() โ†’ GET /booking/my/upcoming Pinia Store (stores/user.ts) โ”œโ”€โ”€ State: โ”‚ โ”œโ”€โ”€ user: UserProfileResponse | null โ”‚ โ”œโ”€โ”€ memberships: MembershipWithCardType[] โ”‚ โ”œโ”€โ”€ token: string โ”‚ โ””โ”€โ”€ stats: UserStatsResponse | null โ”‚ โ”œโ”€โ”€ Computed: โ”‚ โ”œโ”€โ”€ loggedIn: boolean โ”‚ โ”œโ”€โ”€ hasValidMembership: boolean โ”‚ โ””โ”€โ”€ activeMemberships: MembershipWithCardType[] โ”‚ โ””โ”€โ”€ Actions: โ”œโ”€โ”€ login() โ†’ WX login + token โ”œโ”€โ”€ fetchMemberships() โ†’ GET /membership/my โ”œโ”€โ”€ fetchProfile() โ†’ GET /user/profile โ””โ”€โ”€ logout() ``` --- ## ๐Ÿ“ก API Calls Sequence ``` INITIAL LOAD โ”œโ”€ POST /auth/wxLogin โ”‚ โ””โ”€ Returns: { token, user } โ”‚ โ”œโ”€ GET /membership/my (if logged in) โ”‚ โ””โ”€ Returns: MembershipWithCardType[] โ”‚ โ””โ”€ GET /time-slot/available?date=TODAY โ””โ”€ Returns: TimeSlotWithBookingStatus[] DATE CHANGE โ””โ”€ GET /time-slot/available?date=SELECTED_DATE โ””โ”€ Returns: TimeSlotWithBookingStatus[] BOOKING CREATION โ”œโ”€ POST /booking โ”‚ โ”œโ”€ Body: { timeSlotId, membershipId } โ”‚ โ””โ”€ Returns: BookingWithDetails โ”‚ โ””โ”€ GET /time-slot/available?date=SELECTED_DATE (refresh) โ””โ”€ Returns: Updated slots with isBookedByMe: true BOOKING CANCELLATION โ”œโ”€ PUT /booking/:bookingId/cancel โ”‚ โ””โ”€ Returns: Updated BookingWithDetails โ”‚ โ””โ”€ GET /time-slot/available?date=SELECTED_DATE (refresh) โ””โ”€ Returns: Updated slots with isBookedByMe: false ``` --- ## ๐ŸŽญ Slot Card State Machine ``` TimeSlotWithBookingStatus { status: 'OPEN' | 'FULL' | 'CLOSED' isBookedByMe: boolean } STATE COMBINATIONS: โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ status: OPEN, isBookedByMe: false โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ Button: "ๅฏ้ข„็บฆ" (Tan) โ”‚ โ”‚ Color: #c9a87c โ”‚ โ”‚ Action: onBookTap() โ†’ Popup โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ status: OPEN, isBookedByMe: true โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ Badge: "ๅทฒ้ข„็บฆ" โ”‚ โ”‚ Link: "ๅ–ๆถˆ" (Red underline) โ”‚ โ”‚ Indicator: Tan bar on left โ”‚ โ”‚ Action: onCancelTap() โ†’ Confirm โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ status: FULL โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ Button: "ๅทฒ็บฆๆปก" (Gray) โ”‚ โ”‚ Color: #f0f0f0 โ”‚ โ”‚ Action: Disabled (no-op) โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ status: CLOSED โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ Button: "ๅทฒๅ…ณ้—ญ" (Gray) โ”‚ โ”‚ Color: #f0f0f0 โ”‚ โ”‚ Action: Disabled (no-op) โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` --- ## ๐Ÿ“Š Capacity Label Colors ``` Condition Label Background Text โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ status === CLOSED "ๅทฒๅ…ณ้—ญ" #f5f5f5 #999 status === FULL "0/1 ไบบ" #fef0f0 #ef4444 bookedCount >= 80% "0/1 ไบบ" #fff8ed #f59e0b bookedCount < 80% "0/1 ไบบ" #f0faf3 #4caf50 ``` --- ## ๐ŸŒ Time Period Filters ``` Key Label Start End Range โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ null (all) "ๅ…จ้ƒจ" - - All times 'MORNING' "ไธŠๅˆ" 06:00 12:00 6am-12pm 'AFTERNOON' "ไธ‹ๅˆ" 12:00 18:00 12pm-6pm 'EVENING' "ๆ™šไธŠ" 18:00 22:00 6pm-10pm Filtering Logic: slot.startTime >= period.start && slot.startTime < period.end ``` --- ## ๐Ÿ“ฑ UI Layout Breakdown ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ๐Ÿ“ฑ Booking Page (750rpx) โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚ โ”‚ โ”‚ ๐ŸŽซ STICKY HEADER (z-index:100) โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚ โ”‚ โ”‚ โ”‚ DateSelector (horizontal) โ”‚โ”‚ โ”‚ โ”‚ โ”‚ ไปŠๅคฉ 5ๆœˆ 4ๆœˆ 3ๆœˆ... โ”‚โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚ โ”‚ โ”‚ โ”‚ TimePeriodFilter (tabs) โ”‚โ”‚ โ”‚ โ”‚ โ”‚ ๅ…จ้ƒจ | ไธŠๅˆ | ไธ‹ๅˆ | ๆ™šไธŠโ”‚โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚ โ”‚ โ”‚ ๐Ÿ“œ SCROLL AREA โ”‚โ”‚ โ”‚ โ”‚ โ”‚โ”‚ โ”‚ โ”‚ OR [Loading skeleton] ร—4 โ”‚โ”‚ โ”‚ โ”‚ OR [Empty state] โ”‚โ”‚ โ”‚ โ”‚ โ”‚โ”‚ โ”‚ โ”‚ [SlotCard 1] โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚โ”‚ โ”‚ โ”‚ 09:00-10:00 โ”‚ 0/1 ไบบ โ”‚ โ”‚โ”‚ โ”‚ โ”‚ โ”‚ [ๅฏ้ข„็บฆ] โ”‚ โ”‚โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚โ”‚ โ”‚ โ”‚ [SlotCard 2] โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚โ”‚ โ”‚ โ”‚ 10:00-11:00 โ”‚ 1/1 ไบบ โ”‚ โ”‚โ”‚ โ”‚ โ”‚ โœ“ๅทฒ้ข„็บฆ [ๅ–ๆถˆ]โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚โ”‚ โ”‚ โ”‚ [SlotCard 3] ... โ”‚โ”‚ โ”‚ โ”‚ โ”‚โ”‚ โ”‚ โ”‚ [Spacer 48rpx] โ”‚โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚ โ”‚ โ”‚ [BookingConfirmPopup] (Modal)โ”‚โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚ โ”‚ โ”‚ โ”‚ โœ• ็กฎ่ฎค้ข„็บฆ โ”‚โ”‚ โ”‚ โ”‚ โ”‚ โ”‚โ”‚ โ”‚ โ”‚ โ”‚ ๆ—ฅๆœŸ: 2026-04-05 โ”‚โ”‚ โ”‚ โ”‚ โ”‚ ๆ—ถ้—ด: 09:00 - 10:00 โ”‚โ”‚ โ”‚ โ”‚ โ”‚ ๅ‰ฉไฝ™: 1 ไธชๅ้ข โ”‚โ”‚ โ”‚ โ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚โ”‚ โ”‚ โ”‚ โ”‚ ๐Ÿ’ณ ็งๆ•™่ฏพ็จ‹ โ”‚โ”‚ โ”‚ โ”‚ โ”‚ ๅ‰ฉไฝ™ 10 ๆฌก โœ“ โ”‚โ”‚ โ”‚ โ”‚ โ”‚ ็กฎ่ฎคๅŽๆ‰ฃ้™ค 1 ๆฌก่ฏพๆ—ถ โ”‚โ”‚ โ”‚ โ”‚ โ”‚ โ”‚โ”‚ โ”‚ โ”‚ โ”‚ [ๅ–ๆถˆ] [็กฎ่ฎค้ข„็บฆ] โ”‚โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` --- ## ๐Ÿ” Authentication Flow ``` PAGE LOAD โ”‚ โ”œโ”€ Check: userStore.loggedIn? โ”‚ โ”œโ”€ YES โ”‚ โ”œโ”€ Check: userStore.activeMemberships.length > 0? โ”‚ โ”‚ โ”œโ”€ NO: await fetchMemberships() โ”‚ โ”‚ โ””โ”€ YES: (already loaded) โ”‚ โ”‚ โ”‚ โ””โ”€ Load today's slots โ”‚ โ””โ”€ NO (not logged in) โ””โ”€ Page loads but booking disabled (onBookTap shows login modal) USER TAPS "ๅฏ้ข„็บฆ" โ”‚ โ”œโ”€ Check: userStore.loggedIn? โ”‚ โ”œโ”€ NO: Show login modal โ”‚ โ”‚ โ”œโ”€ User confirms โ†’ wxLogin() โ”‚ โ”‚ โ”œโ”€ Retry booking flow โ”‚ โ”‚ โ””โ”€ Success: Load memberships, show popup โ”‚ โ”‚ โ”‚ โ””โ”€ YES: Continue โ”‚ โ”œโ”€ Check: userStore.hasValidMembership? โ”‚ โ”œโ”€ NO: Show purchase modal โ”‚ โ”‚ โ””โ”€ User navigates to /pages/store/index โ”‚ โ”‚ โ”‚ โ””โ”€ YES: Continue โ”‚ โ””โ”€ Show BookingConfirmPopup ``` --- ## โš™๏ธ Error Handling (Current) ``` fetchSlots() Error: โ”œโ”€ console.error('Fetch slots failed:', err) โ”œโ”€ slots.value = [] โ””โ”€ UI shows: "ๅฝ“ๆ—ฅๆš‚ๆ— ๅฏ็บฆๆ—ถๆฎต" (empty state) โŒ User can't distinguish network error from no slots createBooking() Error: โ”œโ”€ uni.showToast({ title: message, icon: 'none' }) โ””โ”€ UI shows: Error toast (Good โœ“) cancelBooking() Error: โ”œโ”€ uni.showToast({ title: message, icon: 'none' }) โ””โ”€ UI shows: Error toast (Good โœ“) ``` --- ## ๐Ÿงฎ Computed Values & Reactivity ``` PAGE LEVEL: scrollHeight = computed(() => { // Recalc when window size changes // = windowHeight - headerHeight - tabbarHeight }) filteredSlots = computed(() => { // Depends on: slots, selectedPeriod // Recalc when either changes // Filters by TIME_PERIODS[selectedPeriod].start/end }) COMPONENT LEVEL: SlotCard.capacityLabel = computed(() => { // Depends on: slot.status, slot.bookedCount, slot.capacity // Returns: "ๅทฒๅ…ณ้—ญ" | "X/Y ไบบ" }) SlotCard.capacityClass = computed(() => { // Depends on: slot.status, slot.bookedCount, slot.capacity // Returns: "cap-open" | "cap-almost" | "cap-full" | "cap-closed" }) BookingConfirmPopup.selectedMembership = computed(() => { // Depends on: selectedMembershipId, memberships // Returns: Found membership or null }) ``` --- ## ๐ŸŽฏ Key Data Transformations ``` Raw API Response โ””โ”€ TimeSlot { date: "2026-04-05", startTime: "09:00", endTime: "10:00", ... } STORE (bookingStore.slots) โ””โ”€ TimeSlotWithBookingStatus extends TimeSlot { isBookedByMe: boolean, myBookingId: string | null } DISPLAY (SlotCard) โ”œโ”€ capacityLabel: "0/1 ไบบ" | "ๅทฒๅ…ณ้—ญ" โ”œโ”€ capacityClass: "cap-open" | "cap-almost" | "cap-full" | "cap-closed" โ”œโ”€ Button state: "ๅฏ้ข„็บฆ" | "ๅทฒ้ข„็บฆ" | "ๅทฒ็บฆๆปก" | "ๅทฒๅ…ณ้—ญ" โ””โ”€ Time display: "09:00 - 10:00" (slice first 5 chars) BOOKING CREATION โ”œโ”€ Selected Slot ID โ”œโ”€ Selected Membership ID โ””โ”€ POST /booking โ””โ”€ Success: Slot updated with isBookedByMe: true ```