From b6986ba30c377858713aa289d38e7bf7c441627f Mon Sep 17 00:00:00 2001 From: richarjiang Date: Sun, 5 Apr 2026 12:18:49 +0800 Subject: [PATCH] feat(admin): implement full day-by-day schedule editor with live preview ## Features ### Admin Schedule Page (`packages/app/src/pages/admin/schedule.vue`) - Interactive date-based slot editor for managing daily schedules - Real-time slot editing: start/end times, capacity adjustments - Slot deletion with conflict warnings when bookings exist - Add new slots with modal dialog - Live booking status display (booked count, people names) - Publish/Save changes with sync feedback - Revert unsaved changes with confirmation - Skeleton loading states and empty state handling - Responsive design with optimized mobile UX ### Backend Enhancements - **New DTO** (`PublishDaySlotsDto`): Structured slot publishing with validation - Date string validation - Slot array with existing slot IDs for updates - Time and capacity validation per slot - **Schedule Preview API** (`getSchedulePreview`): - Check for existing published slots - Fallback to active WeekTemplates for unpublished dates - Unified response format with isPublished flag - **Publish Slots API** (`publishDaySlots`): - Atomic transaction for consistency - Update existing slots with new times/capacity - Create new slots from template data - Delete unpublished slots or set to CLOSED if bookings exist - Prevent capacity reduction below existing bookings - Returns all published slots for feedback ### State Management - Enhanced admin store with schedule state - Support for pending/unsaved slot changes - Optimistic UI updates with server sync ### Documentation - Comprehensive scheduling system architecture docs - Quick reference for admin workflows - Flow diagrams and state transitions - Implementation guide for future maintenance ## Breaking Changes None ## Testing Recommendations - Create slots for future dates via schedule editor - Verify booking prevention for locked/full slots - Test capacity adjustments with existing bookings - Confirm template-based schedule generation - Verify transaction rollback on publish failures Co-Authored-By: Claude Opus 4.6 (1M context) --- ADMIN_SCHEDULING_EXPLORATION.md | 803 +++++++++++++++ BOOKING_ARCHITECTURE_DIAGRAM.md | 552 ++++++++++ BOOKING_PAGE_ANALYSIS.md | 894 ++++++++++++++++ BOOKING_README.md | 395 +++++++ COMPONENT_HIERARCHY.md | 359 +++++++ QUICK_REFERENCE.md | 592 +++++++++++ SCHEDULING_DOCUMENTATION_INDEX.md | 244 +++++ SCHEDULING_FLOW_DIAGRAM.md | 271 +++++ SCHEDULING_QUICK_REFERENCE.md | 296 ++++++ docs/TIME_SLOT_DIAGRAMS.md | 606 +++++++++++ docs/TIME_SLOT_INDEX.md | 364 +++++++ docs/TIME_SLOT_QUICK_REFERENCE.md | 355 +++++++ docs/TIME_SLOT_SCHEDULING_SYSTEM.md | 966 ++++++++++++++++++ packages/app/src/components/QuickEntry.vue | 2 + packages/app/src/components/UserCard.vue | 12 +- packages/app/src/pages.json | 8 +- packages/app/src/pages/admin/index.vue | 4 +- packages/app/src/pages/admin/schedule.vue | 755 ++++++++++++++ .../app/src/pages/admin/week-template.vue | 39 +- packages/app/src/pages/profile/info.vue | 15 +- packages/app/src/stores/admin.ts | 29 +- .../time-slot/dto/publish-day-slots.dto.ts | 37 + .../src/time-slot/time-slot.controller.ts | 14 + .../server/src/time-slot/time-slot.service.ts | 168 ++- packages/server/tsconfig.build.tsbuildinfo | 2 +- packages/shared/src/index.ts | 3 + packages/shared/src/types/index.ts | 2 +- packages/shared/src/types/time-slot.ts | 32 + pnpm-lock.yaml | 10 + 29 files changed, 7810 insertions(+), 19 deletions(-) create mode 100644 ADMIN_SCHEDULING_EXPLORATION.md create mode 100644 BOOKING_ARCHITECTURE_DIAGRAM.md create mode 100644 BOOKING_PAGE_ANALYSIS.md create mode 100644 BOOKING_README.md create mode 100644 COMPONENT_HIERARCHY.md create mode 100644 QUICK_REFERENCE.md create mode 100644 SCHEDULING_DOCUMENTATION_INDEX.md create mode 100644 SCHEDULING_FLOW_DIAGRAM.md create mode 100644 SCHEDULING_QUICK_REFERENCE.md create mode 100644 docs/TIME_SLOT_DIAGRAMS.md create mode 100644 docs/TIME_SLOT_INDEX.md create mode 100644 docs/TIME_SLOT_QUICK_REFERENCE.md create mode 100644 docs/TIME_SLOT_SCHEDULING_SYSTEM.md create mode 100644 packages/app/src/pages/admin/schedule.vue create mode 100644 packages/server/src/time-slot/dto/publish-day-slots.dto.ts diff --git a/ADMIN_SCHEDULING_EXPLORATION.md b/ADMIN_SCHEDULING_EXPLORATION.md new file mode 100644 index 0000000..e5e638b --- /dev/null +++ b/ADMIN_SCHEDULING_EXPLORATION.md @@ -0,0 +1,803 @@ +# WeChat Mini-Program Admin Scheduling/ๆŽ’่ฏพ่ฎพ็ฝฎ - Complete Exploration Report + +**Date**: 2026-04-05 +**Project**: mp-pilates (WeChat mini-program for pilates studio bookings) + +--- + +## ๐Ÿ“‹ Executive Summary + +This is a **Pilates studio booking management system** with a comprehensive admin scheduling UI. The "ๆŽ’่ฏพ่ฎพ็ฝฎ" (Schedule Setup) feature allows admins to: +1. Define recurring weekly class templates (ๆ—ถ้—ดๆจกๆฟ) +2. Manually add time slots for specific dates +3. Close slots (ไธดๆ—ถ่ฐƒๆ•ด โ†’ ๅ…ณ้—ญๆ—ถๆฎต) +4. Batch generate slots from templates + +The architecture uses: +- **Frontend**: Vue 3 + TypeScript (WeChat mini-program with Taro/UNI framework) +- **Backend**: NestJS + Prisma ORM +- **State Management**: Pinia (Vue state management) +- **Database**: Likely PostgreSQL/MySQL with Prisma + +--- + +## ๐Ÿ—‚๏ธ File Structure + +### Frontend Admin Pages +``` +packages/app/src/pages/admin/ +โ”œโ”€โ”€ index.vue # Admin dashboard with nav grid +โ”œโ”€โ”€ week-template.vue # ๐Ÿ“… Scheduling/ๆŽ’่ฏพ่ฎพ็ฝฎ - Main feature +โ”œโ”€โ”€ slot-adjust.vue # ๐Ÿ”ง Temporary adjustments (3 tabs) +โ”œโ”€โ”€ members.vue # ๐Ÿ‘ฅ Member management +โ”œโ”€โ”€ orders.vue # ๐Ÿ“‹ Order management +โ”œโ”€โ”€ card-types.vue # ๐Ÿ’ณ Card type management +โ””โ”€โ”€ studio.vue # ๐Ÿข Studio settings +``` + +### Stores +``` +packages/app/src/stores/ +โ””โ”€โ”€ admin.ts # Pinia store with all admin API calls +``` + +### Backend API +``` +packages/server/src/ +โ”œโ”€โ”€ time-slot/ +โ”‚ โ”œโ”€โ”€ time-slot.controller.ts # Admin & member endpoints for slots +โ”‚ โ”œโ”€โ”€ time-slot.service.ts # Business logic for slots +โ”‚ โ”œโ”€โ”€ slot-generator.service.ts # Template-based slot generation +โ”‚ โ””โ”€โ”€ dto/ +โ”‚ โ”œโ”€โ”€ week-template.dto.ts # Input validation +โ”‚ โ”œโ”€โ”€ create-manual-slot.dto.ts +โ”‚ โ””โ”€โ”€ query-slots.dto.ts +โ”œโ”€โ”€ studio/ +โ”‚ โ””โ”€โ”€ studio.controller.ts # Studio config (admin endpoints) +โ””โ”€โ”€ scheduler/ # Cron scheduler for auto-generation +``` + +### Shared Types +``` +packages/shared/src/types/ +โ”œโ”€โ”€ week-template.ts # WeekTemplate interface +โ”œโ”€โ”€ time-slot.ts # TimeSlot interface +โ””โ”€โ”€ constants.ts # WEEKDAY_LABELS, SLOT_GENERATION_DAYS +``` + +--- + +## ๐Ÿ”‘ Key Components + +### 1. **Admin Dashboard (index.vue)** + +**File**: `packages/app/src/pages/admin/index.vue` + +**Features**: +- Display stats: today's bookings, total orders, total bookings +- Navigation grid to 6 admin modules: + - ๐Ÿ“… **ๆŽ’่ฏพ่ฎพ็ฝฎ** โ†’ `/pages/admin/week-template` + - ๐Ÿ”ง **ไธดๆ—ถ่ฐƒๆ•ด** โ†’ `/pages/admin/slot-adjust` + - ๐Ÿ‘ฅ **ไผšๅ‘˜็ฎก็†** โ†’ `/pages/admin/members` + - ๐Ÿ“‹ **่ฎขๅ•็ฎก็†** โ†’ `/pages/admin/orders` + - ๐Ÿ’ณ **ๅก็ง็ฎก็†** โ†’ `/pages/admin/card-types` + - ๐Ÿข **ๅทฅไฝœๅฎค่ฎพ็ฝฎ** โ†’ `/pages/admin/studio` + +**Key Functions**: +```typescript +- navigate(path): Navigates to admin pages +- loadStats(): Fetches dashboard statistics via adminStore.fetchDashboardStats() +``` + +**State**: +```typescript +const stats = ref({ todayBookings: 0, totalOrders: 0, totalBookings: 0 }) +const statsLoading = ref(false) +``` + +--- + +### 2. **Week Template Management (week-template.vue)** โœจ MAIN SCHEDULING UI + +**File**: `packages/app/src/pages/admin/week-template.vue` +**Route**: `/pages/admin/week-template` + +#### Purpose +Manage recurring weekly schedule templates. These are used to **auto-generate** time slots for future weeks. + +#### Data Structure +```typescript +interface WeekTemplate { + readonly id: string + readonly dayOfWeek: number // 1=Mon, 2=Tue, ..., 7=Sun (ISO format) + readonly startTime: string // HH:MM format + readonly endTime: string // HH:MM format + readonly capacity: number // Max bookings per slot + readonly isActive: boolean // Enable/disable template + readonly createdAt: string + readonly updatedAt: string +} +``` + +#### UI Sections +1. **Toolbar** + - Display template count: "ๅ…ฑ N ๆกๆจกๆฟ" + - "+ ๆ–ฐๅขžๆ—ถๆฎต" (Add new slot) button + +2. **Template List** (Grouped by weekday) + - Days are sorted (Monday โ†’ Sunday) + - Each day shows count: "3 ไธชๆ—ถๆฎต" + - Each template row displays: + - Time range: "09:00 โ€“ 10:00" + - Capacity: "10 ไบบ" + - Actions: + - Toggle button: "ๅฏ็”จ"/"ๅœ็”จ" (Enable/Disable) + - "็ผ–่พ‘" (Edit) + - "ๅˆ ้™ค" (Delete) + - Inactive templates are grayed out (opacity: 0.5) + +3. **Modal for Add/Edit** + - Star date picker (1-7 for weekday) + - Start time picker + - End time picker + - Capacity input (number) + - Validation: time and capacity required + - Cancel/Confirm buttons + +4. **Save Bar** (Fixed at bottom) + - Only shows when `isDirty` flag is true + - "ไฟๅญ˜ๅ…จ้ƒจๆ›ดๆ”น" button with loading state + +#### Key Functions + +```typescript +async fetchTemplates() + - Fetches all templates from backend + - Groups by dayOfWeek for display + - Clears isDirty flag + +async handleSave() + - Maps local template state to API payload + - Calls adminStore.saveWeekTemplates(payload) + - Refreshes templates after save + - Shows success/error toast + +function openAdd() + - Opens modal for creating new template + - Clears form + +function openEdit(tpl) + - Opens modal in edit mode + - Populates form with existing values + +function submitForm() + - Validates form (time and capacity required) + - Creates or updates template in memory + - Sets isDirty = true (triggers save bar) + +function toggleTemplate(tpl) + - Toggles isActive flag + - Sets isDirty = true + +function deleteTemplate(tpl) + - Shows confirmation modal + - Removes from array + - Sets isDirty = true +``` + +#### Local State Management +```typescript +const templates = ref([]) +const loading = ref(false) +const saving = ref(false) +const isDirty = ref(false) // Tracks unsaved changes +const showModal = ref(false) +const editTarget = ref(null) + +const form = ref({ + dayIdx: 0, // Selected day index (0-6) + startTime: '09:00', + endTime: '10:00', + capacityStr: '10', +}) + +const grouped = computed(() => { + // Groups templates by dayOfWeek for rendering + return Object.fromEntries( + Object.entries(map).sort(([a], [b]) => Number(a) - Number(b)) + ) +}) +``` + +#### Example: Adding a Monday 9AM-10AM class +1. User taps "+ ๆ–ฐๅขžๆ—ถๆฎต" +2. Modal opens, form is reset to defaults +3. User selects "ๅ‘จไธ€" (Monday) from picker +4. User confirms times and capacity +5. New template object is pushed to `templates` array +6. `isDirty` is set to true โ†’ save bar appears +7. User taps "ไฟๅญ˜ๅ…จ้ƒจๆ›ดๆ”น" +8. Store calls `PUT /admin/week-template` with all templates +9. Backend deletes all old templates and creates new ones +10. Frontend refetches and displays updated list + +--- + +### 3. **Slot Adjustment (slot-adjust.vue)** - Temporary Slot Management + +**File**: `packages/app/src/pages/admin/slot-adjust.vue` +**Route**: `/pages/admin/slot-adjust` + +#### Purpose +Handle temporary/manual time slot operations: +1. Add one-off slots for specific dates +2. Close available slots +3. Batch-generate slots from templates + +#### UI Structure (3 Tabs) + +##### Tab 0: "ๆ–ฐๅขžๆ—ถๆฎต" (Add Manual Slot) +- Date picker (defaults to today) +- Start/End time pickers +- Capacity input +- Submit button: "ๆ–ฐๅขžๆ—ถๆฎต" +- **Endpoint**: `POST /admin/time-slot/manual` + +```typescript +interface CreateManualSlotDto { + date: string // YYYY-MM-DD + startTime: string // HH:MM + endTime: string // HH:MM + capacity?: number // Defaults to DEFAULT_SLOT_CAPACITY (1) +} +``` + +##### Tab 1: "ๅ…ณ้—ญๆ—ถๆฎต" (Close Slots) +- Date picker (defaults to today) +- Loads all slots for selected date +- Displays slot list with: + - Time range + - Status badge (OPEN/FULL/CLOSED) + - Booked count: "X/Y" + - Close button (if not already closed) +- Confirmation modal when closing +- **Endpoint**: `PUT /admin/time-slot/:id/close` + +**Slot Status Colors**: +``` +OPEN โ†’ Green badge #27ae60 +FULL โ†’ Orange badge #e67e22 +CLOSED โ†’ Gray badge #999 +``` + +##### Tab 2: "ๆ‰น้‡็”Ÿๆˆ" (Batch Generate) +- Start date picker +- End date picker (defaults to +7 days) +- Hint: "ๅฐ†ๆ นๆฎๆŽ’่ฏพๆจกๆฟ๏ผŒ่‡ชๅŠจ็”Ÿๆˆๆ‰€้€‰ๆ—ฅๆœŸ่Œƒๅ›ดๅ†…็š„ๆ—ถๆฎต" + - *"Will auto-generate slots for selected date range based on schedule template"* +- Submit button: "ๆ‰น้‡็”Ÿๆˆ" +- **Endpoint**: `POST /admin/generate-slots` + +**How it works**: +1. Frontend sends date range to backend +2. Backend fetches all **active** WeekTemplates +3. For each day in range, finds matching templates by weekday +4. Creates TimeSlot records with `source: TEMPLATE` +5. Uses `skipDuplicates: true` to avoid re-generating existing slots + +```typescript +// Backend example: If templates include: +// - Monday: 09:00-10:00, 18:00-19:00 (2 templates) +// - Wednesday: 10:00-11:00 (1 template) +// +// And user selects 2026-04-05 to 2026-04-11: +// - Mon 04-06: 2 slots generated +// - Wed 04-08: 1 slot generated +// Total: 3 slots (if these dates fall in range) +``` + +#### Key Functions +```typescript +async submitAddSlot() + - POST /admin/time-slot/manual + - Shows success/error toast + +async loadSlotsForClose() + - Fetches slots for closeDate via adminStore.fetchSlotsByDate(date) + - Sets slotsLoading flag + +async closeSlot(slot) + - Confirmation modal + - PUT /admin/time-slot/:id/close + - Reloads slot list + +async submitGenerate() + - POST /admin/generate-slots with date range + - Shows toast with count of generated slots +``` + +#### Local State +```typescript +const activeTab = ref(0) // 0=Add, 1=Close, 2=Generate +const submitting = ref(false) +const slotsLoading = ref(false) + +// Tab 0: Add form +const addForm = ref({ + date: formatDate(new Date()), + startTime: '09:00', + endTime: '10:00', + capacityStr: '10', +}) + +// Tab 1: Close slots +const closeDate = ref(formatDate(new Date())) +const daySlots = ref([]) + +// Tab 2: Generate form +const genForm = ref({ + startDate: formatDate(new Date()), + endDate: formatDate(new Date(Date.now() + 7 * 86400000)), // +7 days +}) +``` + +--- + +### 4. **Admin Store (Pinia)** + +**File**: `packages/app/src/stores/admin.ts` + +#### API Methods Related to Scheduling + +```typescript +// โ”€โ”€ Week templates โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +async fetchWeekTemplates(): Promise + // GET /admin/week-template + // Returns all templates for current studio + // Usage: Gets templates for display in week-template.vue + +async saveWeekTemplates(templates: WeekTemplateInput[]): Promise + // PUT /admin/week-template + // Body: { templates: [...] } + // Replaces ALL templates with new set (delete all, create new) + // Note: Backend uses transaction for atomicity + +// โ”€โ”€ Time slots โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +async fetchSlotsByDate(date: string): Promise + // GET /admin/time-slots?date=YYYY-MM-DD + // Returns all slots for a specific date + // Used in slot-adjust.vue Tab 1 (close slots) + +async createManualSlot(dto: CreateManualSlotDto): Promise + // POST /admin/time-slot/manual + // Creates a one-off time slot + // Used in slot-adjust.vue Tab 0 + +async closeSlot(id: string): Promise + // PUT /admin/time-slot/:id/close + // Changes slot status from OPEN to CLOSED + // Used in slot-adjust.vue Tab 1 + +async generateSlots(startDate: string, endDate: string): Promise<{ count: number }> + // POST /admin/generate-slots + // Generates slots from active templates for date range + // Used in slot-adjust.vue Tab 2 + // Returns: { count: number of newly created slots } + +// โ”€โ”€ Dashboard โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +async fetchDashboardStats(): Promise + // GET /admin/stats + // Returns: { todayBookings, totalOrders, totalBookings } + // Used in index.vue +``` + +#### API Response Types +```typescript +interface AdminStats { + todayBookings: number + totalOrders: number + totalBookings: number +} + +interface WeekTemplate { + id: string + dayOfWeek: number // 1-7 (ISO weekday) + startTime: string // HH:MM + endTime: string // HH:MM + capacity: number + isActive: boolean + createdAt: string + updatedAt: string +} + +interface TimeSlot { + id: string + date: string // YYYY-MM-DD + startTime: string + endTime: string + capacity: number + bookedCount: number + status: TimeSlotStatus // OPEN | FULL | CLOSED + source: TimeSlotSource // TEMPLATE | MANUAL + templateId: string | null + createdAt: string + updatedAt: string +} +``` + +--- + +## ๐Ÿ”Œ Backend Architecture + +### Time Slot Controller +**File**: `packages/server/src/time-slot/time-slot.controller.ts` + +#### Member Endpoints (Public) +``` +GET /time-slot/available?date=YYYY-MM-DD + - Get available slots for a date + - Include booking status for current user + +GET /time-slot/:id + - Get specific slot details +``` + +#### Admin Endpoints (Requires JWT + ADMIN role) +``` +GET /admin/week-template + - Returns all WeekTemplates + - Ordered by: dayOfWeek ASC, startTime ASC + +PUT /admin/week-template + - Request body: { templates: [...] } + - Replaces all templates (transaction-based) + - Validation: dayOfWeek 1-7, startTime/endTime strings + +POST /admin/time-slot/manual + - Request body: { date, startTime, endTime, capacity? } + - Creates manual slot with source=MANUAL + - Capacity defaults to DEFAULT_SLOT_CAPACITY + +PUT /admin/time-slot/:id/close + - Changes slot status to CLOSED + - Returns updated slot + +POST /admin/generate-slots + - Generates slots from active templates + - Fetches templates where isActive=true + - Creates slots for next SLOT_GENERATION_DAYS (14 days by default) + - Uses skipDuplicates to make re-runs safe + - Returns: { count: number } +``` + +### Time Slot Service +**File**: `packages/server/src/time-slot/time-slot.service.ts` + +Key methods: +```typescript +async getWeekTemplates(): Promise + // Returns all templates sorted by day/time + +async replaceWeekTemplates(items: Array<{...}>): Promise + // Transaction-based replacement: + // 1. Delete all existing templates + // 2. Create new ones from items array + // 3. Return count of created + +async createManualSlot(dto): Promise + // Creates slot with source=MANUAL, status=OPEN + +async closeSlot(id: string): Promise + // Updates status to CLOSED +``` + +### Slot Generator Service +**File**: `packages/server/src/time-slot/slot-generator.service.ts` + +Key method: +```typescript +async generateSlots(daysAhead: number = 14): Promise + // 1. Fetches all WeekTemplates where isActive=true + // 2. For each of next N days: + // - Calculate ISO weekday (1=Mon, 7=Sun) + // - Find matching templates by dayOfWeek + // - Create TimeSlot records with source=TEMPLATE, templateId=id + // 3. Uses createMany with skipDuplicates=true + // 4. Returns count of newly created slots + // + // Key: Converts JS getDay() (0=Sun) to ISO weekday (1=Mon, 7=Sun) + +async cleanupExpiredSlots(): Promise + // Called by scheduler + // Closes all OPEN slots with date < today + +async checkExpiredMemberships(): Promise + // Called by scheduler + // Expires memberships past end date or with 0 sessions left + +async completeBookings(): Promise + // Called by scheduler + // Marks CONFIRMED bookings as COMPLETED if slot date passed +``` + +--- + +## ๐Ÿ“Š Data Flow: "ๆŽ’่ฏพ่ฎพ็ฝฎ" User Journey + +### Scenario: Admin sets up class schedule for next week + +1. **Admin opens dashboard** โ†’ `index.vue` + - Taps "ๆŽ’่ฏพ่ฎพ็ฝฎ" nav item + +2. **Admin navigates to Week Template page** โ†’ `week-template.vue` + - `onMounted()` โ†’ `fetchTemplates()` + - Frontend: `GET /admin/week-template` + - Shows existing templates grouped by day + - Example display: + ``` + ๅ‘จไธ€ + 09:00-10:00 10ไบบ [ๅฏ็”จ] [็ผ–่พ‘] [ๅˆ ้™ค] + 18:00-19:00 8ไบบ [ๅฏ็”จ] [็ผ–่พ‘] [ๅˆ ้™ค] + ๅ‘จไธ‰ + 10:00-11:00 12ไบบ [ๅฏ็”จ] [็ผ–่พ‘] [ๅˆ ้™ค] + ``` + +3. **Admin adds a new class** โ†’ Click "+ ๆ–ฐๅขžๆ—ถๆฎต" + - Modal opens + - Select day, time, capacity + - Click "็กฎ่ฎค" + - Template added to local `templates` array + - **Save bar appears** at bottom + +4. **Admin edits existing template** โ†’ Click "็ผ–่พ‘" + - Modal opens with existing values + - Modify time/capacity + - Click "็กฎ่ฎค" + - Updated in local array + - Save bar shows if changed + +5. **Admin disables a template** โ†’ Click "ๅœ็”จ" + - `isActive` flipped to false + - Template grayed out + - Save bar shows + +6. **Admin saves all changes** โ†’ Click "ไฟๅญ˜ๅ…จ้ƒจๆ›ดๆ”น" + - Loading state + - Frontend: `PUT /admin/week-template` with all templates + - Backend transaction: + ``` + BEGIN TRANSACTION + DELETE FROM week_template + INSERT INTO week_template (day_of_week, start_time, end_time, capacity, is_active) VALUES (...) + COMMIT TRANSACTION + ``` + - Success toast + - Frontend refetches templates + - Save bar disappears + +7. **Backend scheduler auto-generates slots** + - Nightly cron (scheduler module) + - Calls `SlotGeneratorService.generateSlots(14)` + - Queries active WeekTemplates + - For each day in next 14 days: + - Checks what templates apply (by ISO weekday) + - Creates TimeSlot records + - Uses `skipDuplicates` to avoid duplicates on re-run + - Example output: + ``` + date: 2026-04-06 (Monday) + 09:00-10:00 source=TEMPLATE templateId=abc123 + 18:00-19:00 source=TEMPLATE templateId=def456 + date: 2026-04-08 (Wednesday) + 10:00-11:00 source=TEMPLATE templateId=ghi789 + ``` + +8. **Members can see and book the generated slots** + - Frontend: `GET /time-slot/available?date=2026-04-06` + - Members choose a slot and confirm booking + +--- + +## ๐Ÿ“… Constants & Utilities + +### Shared Constants +**File**: `packages/shared/src/constants.ts` + +```typescript +export const SLOT_GENERATION_DAYS = 14 + // Number of days ahead to generate slots for + +export const DEFAULT_SLOT_CAPACITY = 1 + // Default capacity if not specified (for private lessons) + +export const WEEKDAY_LABELS = ['', 'ๅ‘จไธ€', 'ๅ‘จไบŒ', 'ๅ‘จไธ‰', 'ๅ‘จๅ››', 'ๅ‘จไบ”', 'ๅ‘จๅ…ญ', 'ๅ‘จๆ—ฅ'] + // Index 0 is unused, 1-7 map to weekdays + // Used in dropdowns and display + +export const DEFAULT_CANCEL_HOURS_LIMIT = 2 + // Hours before slot to allow free cancellation + +export const TIME_PERIODS = { + MORNING: { label: 'ไธŠๅˆ', start: '06:00', end: '12:00' }, + AFTERNOON: { label: 'ไธ‹ๅˆ', start: '12:00', end: '18:00' }, + EVENING: { label: 'ๆ™šไธŠ', start: '18:00', end: '22:00' } +} + +export const DATE_SELECTOR_DAYS = 7 +``` + +### Format Utilities +**File**: `packages/app/src/utils/format.ts` + +```typescript +formatDate(date: Date | string): string + // Converts to YYYY-MM-DD format + // Used for date pickers and API calls + +getWeekdayLabel(date: Date | string): string + // Returns Chinese weekday (ๅ‘จไธ€-ๅ‘จๆ—ฅ) + +isToday(date: Date | string): boolean + // Checks if date is today + +getDateRange(days: number): Array<{ date, weekday, isToday }> + // Generates future N days' dates +``` + +### Request Utility +**File**: `packages/app/src/utils/request.ts` + +```typescript +function request(options: RequestOptions): Promise + // Makes HTTP request with JWT auth + // Auto-refreshes token on 401 + +function get(url: string, data?: Record): Promise +function post(url: string, data?: Record): Promise +function put(url: string, data?: Record): Promise +function del(url: string, data?: Record): Promise + +// Base URL logic: +// - Production: https://focus.richarjiang.com/api +// - Development: http://localhost:3000/api +``` + +--- + +## ๐Ÿ” Permission Model + +**Role**: `UserRole.ADMIN` + +### Protected Endpoints +All `/admin/*` endpoints require: +1. Valid JWT token +2. Header: `Authorization: Bearer ` +3. User role must be `ADMIN` + +Protected by: +- `@UseGuards(JwtAuthGuard, RolesGuard)` +- `@Roles(UserRole.ADMIN)` + +### Auth Flow +1. Admin logs in via auth module +2. JWT token returned, stored in `uni.setStorageSync('token')` +3. All requests include token in Authorization header +4. If 401 response: clear token, show login prompt +5. If 4xx/5xx: show error toast + +--- + +## ๐Ÿ› Current Implementation Notes + +### Implemented Features โœ… +- [x] Week template CRUD (Create, Read, Update via replace) +- [x] Manual slot creation +- [x] Close individual slots +- [x] Batch slot generation from templates +- [x] UI for all three slot adjustment tabs +- [x] Local state change tracking (isDirty) +- [x] Modal form for adding/editing templates +- [x] Grouping templates by weekday +- [x] Status badges for slots (OPEN/FULL/CLOSED) + +### Missing/Stub Features โš ๏ธ +- [ ] `fetchDashboardStats()` API endpoint appears to be stubbed + - `index.vue` calls it but endpoint not found in backend + - May need to implement in studio or payment controller +- [ ] No client-side validation errors displayed on API failures +- [ ] No confirmation before overwriting all templates +- [ ] No undo/restore from past template versions + +### Edge Cases to Watch ๐Ÿ” +1. **Timezone handling**: All dates are treated as UTC midnight + - Slot generation uses `setUTCHours(0,0,0,0)` + - Frontend format displays as YYYY-MM-DD (local string) + +2. **Duplicate slot prevention**: + - Backend uses `skipDuplicates: true` in createMany + - Assumes date + startTime + endTime forms unique key + +3. **Template replacement is atomic**: + - All templates deleted, all new ones created in transaction + - If one row fails, entire operation rolls back + +4. **ISO weekday vs JS getDay()**: + - Shared code uses ISO: 1=Mon, 7=Sun + - Frontend picker displays Chinese labels + - Backend slot-generator converts JS getDay() to ISO + +--- + +## ๐Ÿ“ฑ UI Design Patterns + +### Colors & Styling +- **Primary**: `#1a1a2e` (dark navy) +- **Accent**: `#c9a87c` (gold) +- **Success**: `#27ae60` (green) +- **Warning**: `#e67e22` (orange) +- **Danger**: `#c0392b` (red) +- **Background**: `#f5f3f0` (light beige) + +### Component Patterns +1. **Skeleton loaders**: Shimmer animation for loading states +2. **Save bar**: Fixed bottom bar shows only when changes exist +3. **Toggle buttons**: Color indicates state (on=green, off=orange) +4. **Modals**: Bottom-sheet style with backdrop +5. **Pickers**: WeChat native pickers for date/time +6. **Badges**: Color-coded status indicators + +--- + +## ๐Ÿš€ Deployment & Configuration + +### Frontend +- WeChat mini-program environment +- Base URL logic in `packages/app/src/utils/request.ts`: + ```typescript + // Production + https://focus.richarjiang.com/api + + // Development + http://localhost:3000/api + ``` + +### Backend +- NestJS server on port 3000 +- Prisma ORM with database +- JWT authentication +- Role-based access control (RBAC) + +--- + +## ๐Ÿ“š Related Files Summary + +| File | Purpose | Type | +|------|---------|------| +| `admin/index.vue` | Admin dashboard | Component | +| `admin/week-template.vue` | Schedule templates | Component โญ | +| `admin/slot-adjust.vue` | Manual slot ops | Component | +| `stores/admin.ts` | Admin API calls | Store | +| `time-slot.service.ts` | Slot business logic | Service | +| `slot-generator.service.ts` | Template-based generation | Service | +| `time-slot.controller.ts` | API endpoints | Controller | +| `week-template.ts` | Type definitions | Type | +| `constants.ts` | Shared constants | Config | +| `format.ts` | Date/time utilities | Utility | + +--- + +## ๐ŸŽฏ Key Takeaways + +1. **"ๆŽ’่ฏพ่ฎพ็ฝฎ"** is the master schedule template management page +2. **Templates are ISO-weekday based** (1=Monday, 7=Sunday) +3. **Slot generation is automated** via backend scheduler, triggered by: + - Nightly cron job + - Or manual POST to `/admin/generate-slots` endpoint +4. **Save pattern**: Local changes tracked, one "save all" API call with full template array +5. **Timezone**: All operations use UTC midnight as boundaries +6. **Atomicity**: Backend uses Prisma transactions for template replacement +7. **Permissions**: All admin endpoints protected by JWT + ADMIN role guard + diff --git a/BOOKING_ARCHITECTURE_DIAGRAM.md b/BOOKING_ARCHITECTURE_DIAGRAM.md new file mode 100644 index 0000000..80f779c --- /dev/null +++ b/BOOKING_ARCHITECTURE_DIAGRAM.md @@ -0,0 +1,552 @@ +# 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 +``` + diff --git a/BOOKING_PAGE_ANALYSIS.md b/BOOKING_PAGE_ANALYSIS.md new file mode 100644 index 0000000..c70366e --- /dev/null +++ b/BOOKING_PAGE_ANALYSIS.md @@ -0,0 +1,894 @@ +# WeChat Mini-Program Booking Page Analysis +## mp-pilates Project (Uni-app + Vue 3) + +--- + +## ๐Ÿ“‹ Project Structure Overview + +``` +packages/app/src/ +โ”œโ”€โ”€ pages/ +โ”‚ โ””โ”€โ”€ booking/ +โ”‚ โ””โ”€โ”€ index.vue # ๐Ÿ“ Main booking page +โ”œโ”€โ”€ components/ +โ”‚ โ”œโ”€โ”€ DateSelector.vue # Date picker (7 days) +โ”‚ โ”œโ”€โ”€ TimePeriodFilter.vue # Morning/Afternoon/Evening filter +โ”‚ โ”œโ”€โ”€ SlotCard.vue # Individual time slot card +โ”‚ โ””โ”€โ”€ BookingConfirmPopup.vue # Confirmation modal +โ”œโ”€โ”€ stores/ +โ”‚ โ”œโ”€โ”€ booking.ts # ๐Ÿ“ Booking state management +โ”‚ โ””โ”€โ”€ user.ts # User/membership state +โ””โ”€โ”€ utils/ + โ”œโ”€โ”€ request.ts # API request utilities + โ””โ”€โ”€ format.ts # Date/time formatting utilities +``` + +--- + +## ๐ŸŽฏ API Flow + +### Endpoint: `/api/time-slot/available?date=YYYY-MM-DD` + +**Request:** +- Method: `GET` +- Query params: `date` (YYYY-MM-DD format) +- Authentication: Bearer token from localStorage + +**Response Format (from your example):** +```json +{ + "success": true, + "data": [ + { + "id": "string (UUID)", + "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 +} +``` + +**Status Values:** +- `OPEN` - Available to book +- `FULL` - All slots booked +- `CLOSED` - Time slot closed + +**Source Values:** +- `MANUAL` - Manually created +- `TEMPLATE` - Generated from template + +--- + +## ๐Ÿ”„ Complete Data Flow Diagram + +``` +User Opens Booking Page + โ†“ +[onMounted] Lifecycle Hook + โ†“ +1. Check if logged in + fetch memberships (if needed) +2. Load today's slots: bookingStore.fetchSlots(today) + โ†“ +bookingStore.fetchSlots(date: string) + โ†“ +request.get( + '/time-slot/available', + { date } +) + โ†“ +Sets: bookingStore.slots = [TimeSlotWithBookingStatus[], ...] + โ†“ +Vue renders via computed: filteredSlots + โ†“ +User selects date OR filters by time period + โ†“ +Updates: selectedDate.value or selectedPeriod.value + โ†“ +Computed filteredSlots re-calculates + โ†“ +Renders SlotCard components + โ†“ +User taps "ๅฏ้ข„็บฆ" (Book Button) + โ†“ +[onBookTap(slot)] + - Check login (if not โ†’ show login modal) + - Check valid membership (if not โ†’ show purchase modal) + - Show BookingConfirmPopup + โ†“ +User selects membership + confirms + โ†“ +[onConfirmBooking(payload)] + - bookingStore.createBooking({timeSlotId, membershipId}) + - POST /api/booking + - Refresh slots: loadSlots(selectedDate.value) + โ†“ +Success/Error Toast +``` + +--- + +## ๐Ÿ“„ File-by-File Analysis + +### 1๏ธโƒฃ **pages/booking/index.vue** (Main Component) + +**Template Structure:** +``` +.booking-page +โ”œโ”€โ”€ .sticky-header (z-index: 100) +โ”‚ โ”œโ”€โ”€ DateSelector (v-model="selectedDate") +โ”‚ โ””โ”€โ”€ TimePeriodFilter (v-model="selectedPeriod") +โ”œโ”€โ”€ scroll-view.slot-scroll +โ”‚ โ”œโ”€โ”€ Loading skeleton (4 cards) - when loadingSlots +โ”‚ โ”œโ”€โ”€ Empty state - when no slots +โ”‚ โ””โ”€โ”€ SlotCard list - main content +โ”‚ โ””โ”€โ”€ SlotCard (v-for="slot in filteredSlots") +โ””โ”€โ”€ BookingConfirmPopup (conditional) +``` + +**Script Setup - State Variables:** +```typescript +selectedDate: ref // YYYY-MM-DD format +selectedPeriod: ref // 'MORNING'|'AFTERNOON'|'EVENING'|null +showConfirmPopup: ref // Modal visibility +pendingSlot: ref // Slot being booked +refreshing: ref // Pull-to-refresh state +``` + +**Computed Properties:** +```typescript +scrollHeight: computed(() => { + // Calculates scroll area height: + // windowHeight - headerHeight (220rpx) - tabbarHeight (100rpx) + // Converts rpx to pixels dynamically +}) + +filteredSlots: computed(() => { + // If no period selected: return all slots + // If period selected: filter by TIME_PERIODS[selectedPeriod].start/.end + // Compares slot.startTime with period.start and period.end +}) +``` + +**Key Lifecycle - onMounted():** +```typescript +1. If logged in but no memberships fetched yet: + โ†’ await userStore.fetchMemberships() +2. Load today's slots: + โ†’ await loadSlots(formatDate(new Date())) +``` + +**Event Handlers:** + +**onDateSelect(date: string)** โ†’ Changes selectedDate, calls loadSlots() + +**onPeriodChange(period)** โ†’ Updates selectedPeriod (filtering is automatic via computed) + +**onRefresh()** โ†’ Pull-to-refresh handler +```typescript +refreshing.value = true +await loadSlots(selectedDate.value) +refreshing.value = false +``` + +**onBookTap(slot)** โ†’ Book button clicked: +1. Check login status โ†’ show login modal if needed +2. Check hasValidMembership โ†’ show purchase modal if needed +3. Set pendingSlot = slot +4. Show BookingConfirmPopup + +**onConfirmBooking(payload)** โ†’ User confirms booking: +```typescript +await bookingStore.createBooking(payload) +// payload: { timeSlotId, membershipId } +await loadSlots(selectedDate.value) // Refresh +``` + +**onCancelTap(slot)** โ†’ Cancel booking: +```typescript +if (!slot.myBookingId) return +// Show confirmation modal +await bookingStore.cancelBooking(slot.myBookingId) +await loadSlots(selectedDate.value) // Refresh +``` + +**Styles:** +- Page background: `#f5f3f0` (light beige) +- Sticky header with box-shadow +- Loading skeleton with shimmer animation +- Empty state centered with image + +--- + +### 2๏ธโƒฃ **stores/booking.ts** (State Management) + +**State:** +```typescript +slots: ref([]) +myBookings: ref([]) +upcomingBookings: ref([]) +loadingSlots: ref(false) +loadingBookings: ref(false) +``` + +**Actions:** + +**fetchSlots(date: string)** +```typescript +async function fetchSlots(date: string) { + loadingSlots.value = true + try { + slots.value = await get( + '/time-slot/available', + { date } // โ† date as query param + ) + } catch (err) { + console.error('Fetch slots failed:', err) + slots.value = [] + } finally { + loadingSlots.value = false + } +} +``` +โš ๏ธ **CRITICAL:** If request fails, slots.value becomes empty [] + +**createBooking(dto: CreateBookingDto)** +```typescript +// dto: { timeSlotId: string; membershipId: string } +const result = await post('/booking', dto) +return result +``` + +**cancelBooking(bookingId: string)** +```typescript +const result = await put(`/booking/${bookingId}/cancel`) +return result +``` + +**fetchMyBookings(status?: string)** +```typescript +const params = status ? { status } : {} +myBookings.value = await get('/booking/my', params) +``` + +**fetchUpcomingBookings()** +```typescript +upcomingBookings.value = await get('/booking/my/upcoming') +``` + +--- + +### 3๏ธโƒฃ **components/SlotCard.vue** (Individual Slot) + +**Props:** +```typescript +interface Props { + slot: TimeSlotWithBookingStatus +} +``` + +**Emits:** +```typescript +book: [slot] // User wants to book +cancel: [slot] // User wants to cancel +``` + +**Template Sections:** + +**1. Time & Capacity:** +```vue +{{ slot.startTime.slice(0, 5) }} - {{ slot.endTime.slice(0, 5) }} + + + + {{ capacityLabel }} + +``` + +**2. Action Buttons (4 States):** + +**State A: OPEN + not booked by me** +```vue +ๅฏ้ข„็บฆ + +``` + +**State B: OPEN + booked by me** +```vue +ๅทฒ้ข„็บฆ +ๅ–ๆถˆ + +``` + +**State C: FULL** +```vue +ๅทฒ็บฆๆปก + +``` + +**State D: CLOSED** +```vue +ๅทฒๅ…ณ้—ญ + +``` + +**3. Booked Indicator:** +```vue + + +``` + +**Computed Properties:** + +**capacityLabel:** +```typescript +if (status === CLOSED) return 'ๅทฒๅ…ณ้—ญ' +return `${bookedCount}/${capacity} ไบบ` // e.g., "0/1 ไบบ" +``` + +**capacityClass:** Determines background color +``` +CLOSED โ†’ cap-closed (gray) +FULL โ†’ cap-full (red bg, red text) +โ‰ฅ80% โ†’ cap-almost (orange bg, orange text) +<80% โ†’ cap-open (green bg, green text) +``` + +**Styles:** +- Card: white background, 20rpx border-radius, shadow +- Time text: 36rpx, bold, dark +- Capacity badge: 22rpx, inline-flex, colored backgrounds +- Buttons: rounded pills (68rpx height, 34rpx border-radius) +- Cancel text: underlined, red (#ef4444) +- Booked bar: 6rpx tan bar on left edge + +--- + +### 4๏ธโƒฃ **components/DateSelector.vue** (Date Picker) + +**Props:** +```typescript +interface Props { + modelValue: string // YYYY-MM-DD +} +``` + +**Emits:** +- `update:modelValue` - v-model update +- `select` - Custom event on selection + +**Data:** +```typescript +dateRange: computed(() => getDateRange(DATE_SELECTOR_DAYS)) +// DATE_SELECTOR_DAYS = 7 +// Returns array of { date, weekday, isToday } +``` + +**Template:** +```vue + + + + {{ item.isToday ? 'ไปŠๅคฉ' : item.weekday }} + {{ getDayNumber(item.date) }} + {{ getMonthNumber(item.date) }}ๆœˆ + + + +``` + +**Date Display Format:** +- Weekday: "ๅ‘จไธ€", "ๅ‘จไบŒ", or "ไปŠๅคฉ" +- Day: Large bold number (e.g., "5") +- Month: Small number (e.g., "4ๆœˆ") + +**Styles:** +- Active state: tan background (#c9a87c), white text +- Today highlight: tan-colored weekday text (even if not active) +- Horizontal scroll, no scrollbar + +--- + +### 5๏ธโƒฃ **components/TimePeriodFilter.vue** (Period Filter) + +**Props:** +```typescript +type PeriodKey = keyof typeof TIME_PERIODS | null + +interface Props { + modelValue: PeriodKey +} +``` + +**Emits:** +- `update:modelValue` - v-model update +- `change` - Custom event + +**Constants:** +```typescript +const TIME_PERIODS = { + MORNING: { label: 'ไธŠๅˆ', start: '06:00', end: '12:00' }, + AFTERNOON: { label: 'ไธ‹ๅˆ', start: '12:00', end: '18:00' }, + EVENING: { label: 'ๆ™šไธŠ', start: '18:00', end: '22:00' }, +} +``` + +**Tabs Generated:** +```typescript +[ + { key: null, label: 'ๅ…จ้ƒจ' }, + { key: 'MORNING', label: 'ไธŠๅˆ' }, + { key: 'AFTERNOON', label: 'ไธ‹ๅˆ' }, + { key: 'EVENING', label: 'ๆ™šไธŠ' }, +] +``` + +**Template:** +```vue + + {{ tab.label }} + +``` + +**Active State:** +- Text color: tan (#c9a87c), weight: 600 +- Bottom border: 4rpx tan underline (CSS ::after) + +--- + +### 6๏ธโƒฃ **components/BookingConfirmPopup.vue** (Confirmation Modal) + +**Props:** +```typescript +visible: boolean +slot: TimeSlotWithBookingStatus | null +memberships: MembershipWithCardType[] +``` + +**Emits:** +- `confirm` - { timeSlotId, membershipId } +- `cancel` - Popup closes +- `update:visible` - Manual visibility control + +**Template Sections:** + +**1. Overlay Mask:** +```vue + + + +``` + +**2. Header:** +```vue +็กฎ่ฎค้ข„็บฆ +โœ• +``` + +**3. Info Section (read-only display):** +``` +ๆ—ฅๆœŸ: 2026-04-05 +ๆ—ถ้—ด: 09:00 - 10:00 +ๅ‰ฉไฝ™: 1 ไธชๅ้ข +``` + +**4. Membership Card Selection:** + +**Case A: 1 membership** +```vue + + ๐Ÿ’ณ + {{ membership.cardType.name }} + ๅ‰ฉไฝ™ {{ remainingTimes }} ๆฌก + โœ“ + +``` +(Auto-selected, pre-filled) + +**Case B: Multiple memberships** +```vue + + + +``` + +**5. Deduction Tip:** +```vue + + ็กฎ่ฎคๅŽๅฐ†ไปŽใ€Œ{{ selectedMembership.cardType.name }}ใ€ๆ‰ฃ้™ค 1 ๆฌก่ฏพๆ—ถ + +``` + +**6. Action Buttons:** +``` +[ๅ–ๆถˆ] [็กฎ่ฎค้ข„็บฆ] +(Outline) (Tan solid) +(Disabled if no membership selected) +``` + +**Auto-selection Logic:** +```typescript +watch([() => props.visible, () => props.memberships], + ([visible, memberships]) => { + if (visible && memberships.length > 0) { + selectedMembershipId.value = memberships[0].id + } + }, + { immediate: true } +) +``` + +**Confirm Handler:** +```typescript +function handleConfirm() { + emit('confirm', { + timeSlotId: props.slot.id, + membershipId: selectedMembershipId.value, + }) +} +``` + +**Styles:** +- Modal: Fixed positioning, rgba(0,0,0,0.45) dark overlay +- Panel: White background, rounded top corners, 32rpx padding +- Card items: 24rpx padding, border, transition on select +- Buttons: 88rpx height, rounded pills (44rpx) +- Cancel: Outline style, gray text +- Confirm: Solid tan background, white text + +--- + +### 7๏ธโƒฃ **stores/user.ts** (User State) + +**Key State:** +```typescript +user: ref(null) +memberships: ref([]) +token: ref(uni.getStorageSync('token')) +``` + +**Key Computed:** +```typescript +loggedIn: computed(() => !!token.value) +activeMemberships: computed(() => + memberships.value.filter(m => m.status === MembershipStatus.ACTIVE) +) +hasValidMembership: computed(() => activeMemberships.value.length > 0) +``` + +**Key Actions:** +```typescript +async function login() +async function fetchMemberships() + // GET /membership/my +async function logout() +``` + +--- + +### 8๏ธโƒฃ **utils/request.ts** (API Client) + +**Base URL Logic:** +```typescript +const BASE_URL = (() => { + const { miniProgram } = uni.getAccountInfoSync() + if (miniProgram.envVersion !== 'develop') { + return 'https://focus.richarjiang.com/api' + } + return 'http://localhost:3000/api' +})() +``` + +**Main request() function:** +```typescript +function request(options: RequestOptions): Promise { + // 1. Get token from localStorage + const token = uni.getStorageSync('token') + + // 2. Call uni.request with: + // - Authorization header (Bearer token) + // - Content-Type: application/json + + // 3. Response handling: + // - 401 โ†’ Clear token, show "please login", reject + // - โ‰ฅ400 โ†’ Extract error from response.message, reject + // - <400 & success: true โ†’ Resolve with data + // - <400 & success: false โ†’ Reject with message + + // 4. Network fail โ†’ Reject with errMsg +} + +export function get(url, data?): Promise +export function post(url, data?): Promise +export function put(url, data?): Promise +``` + +**โš ๏ธ GET Request Issue:** +```typescript +// In get(), data becomes the request body +// But uni.request with GET should NOT have a body +// Query params should be in the URL string +// This might cause issues on some platforms! +``` + +--- + +### 9๏ธโƒฃ **utils/format.ts** (Date Utilities) + +```typescript +formatDate(date): string + // Returns YYYY-MM-DD + +getWeekdayLabel(date): string + // Returns "ๅ‘จไธ€", "ๅ‘จไบŒ", ..., "ๅ‘จๆ—ฅ" + +isToday(date): boolean + // Compares year/month/day + +getDateRange(days: number): ReadonlyArray + // Returns array of: + // { + // date: YYYY-MM-DD, + // weekday: "ๅ‘จไธ€" | "ไปŠๅคฉ" (if i===0), + // isToday: boolean + // } + // Uses i * 86400000ms for date increment +``` + +--- + +## ๐Ÿ” Data Types Overview + +### TimeSlotWithBookingStatus (Extended from TimeSlot) +```typescript +interface TimeSlotWithBookingStatus extends TimeSlot { + readonly isBookedByMe: boolean // Has user already booked? + readonly myBookingId: string | null // ID needed to cancel +} + +interface TimeSlot { + readonly id: string // UUID + readonly date: string // YYYY-MM-DD + readonly startTime: string // HH:MM + readonly endTime: string // HH:MM + readonly capacity: number // Max people + readonly bookedCount: number // Already booked + readonly status: TimeSlotStatus // OPEN|FULL|CLOSED + readonly source: TimeSlotSource // TEMPLATE|MANUAL + readonly templateId: string | null +} +``` + +### MembershipWithCardType +```typescript +interface MembershipWithCardType { + readonly id: string + readonly cardType: CardType + readonly status: MembershipStatus // ACTIVE|EXPIRED|USED_UP + readonly remainingTimes: number | null + readonly expireDate: string // YYYY-MM-DD +} +``` + +### CreateBookingDto +```typescript +interface CreateBookingDto { + readonly timeSlotId: string + readonly membershipId: string +} +``` + +--- + +## ๐ŸŽจ Color Scheme + +| Element | Color | Hex | Usage | +|---------|-------|-----|-------| +| Primary (Accent) | Tan/Brown | #c9a87c | Buttons, active tabs, highlights | +| Background | Light Beige | #f5f3f0 | Page background | +| Text Primary | Dark Gray | #1a1a1a | Main headings | +| Text Secondary | Medium Gray | #666/#999 | Labels, descriptions | +| Text Tertiary | Light Gray | #bbb | Disabled, hints | +| Success | Green | #4caf50 | Open slots (capacity label) | +| Warning | Orange | #f59e0b | Almost full (capacity label) | +| Error | Red | #ef4444 | Full/closed, cancel button | +| Borders | Very Light Gray | #f0f0f0/#f0ece8 | Dividers, borders | + +--- + +## โš ๏ธ Potential Issues & Problems + +### 1. **GET Request Body Issue** +**File:** `utils/request.ts` in `get()` function +```typescript +export function get(url: string, data?: Record): Promise { + return request({ url, method: 'GET', data }) // โ† data as body! +} +``` +**Problem:** GET requests shouldn't have a body. Query params should be in the URL. +**Impact:** `/time-slot/available?date=2026-04-05` might not work on all platforms. + +### 2. **Empty Slots Array on Error** +**File:** `stores/booking.ts`, `fetchSlots()` +```typescript +catch (err) { + console.error('Fetch slots failed:', err) + slots.value = [] // โ† Clears state on error! +} +``` +**Problem:** Network error โ†’ page shows "empty state" instead of error message. +**Impact:** Users can't tell if there's an error or truly no slots available. + +### 3. **No Error Handling in Main Page** +**File:** `pages/booking/index.vue`, `loadSlots()` +```typescript +async function loadSlots(date: string) { + await bookingStore.fetchSlots(date) + // โ† No error handling, no user feedback +} +``` +**Problem:** If fetchSlots() fails, user sees empty page with no explanation. + +### 4. **Manual Date Calculation** +**File:** `utils/format.ts`, `getDateRange()` +```typescript +const d = new Date(now.getTime() + i * 86400000) +``` +**Problem:** Doesn't account for DST transitions. Using `Date.setDate()` would be safer. + +### 5. **No Loading State for Slots** +**File:** `pages/booking/index.vue` +```typescript + +``` +**Problem:** Skeleton appears only on initial load, not when changing dates or refreshing. +**Impact:** Date changes appear instant (good UX but confusing if slow network). + +### 6. **Hardcoded Membership Message** +**File:** `components/BookingConfirmPopup.vue` +```typescript +็กฎ่ฎคๅŽๅฐ†ไปŽใ€Œ{{ selectedMembership.cardType.name }}ใ€ๆ‰ฃ้™ค 1 ๆฌก่ฏพๆ—ถ +// โ† Always says "1 ๆฌก" even if card might deduct different amounts +``` +**Problem:** Doesn't show actual deduction amount if dynamic. + +--- + +## ๐Ÿ“Š Event Flow Sequence + +``` +1. PAGE LOAD (onMounted) + โ”œโ”€ Check: userStore.loggedIn? + โ”œโ”€ If yes & no memberships: fetchMemberships() + โ””โ”€ loadSlots(today) + โ””โ”€ GET /time-slot/available?date=today + โ””โ”€ bookingStore.slots = [...] + โ””โ”€ render SlotCard components + +2. USER TAPS DATE + โ”œโ”€ selectedDate.value = newDate + โ””โ”€ onDateSelect(newDate) + โ””โ”€ loadSlots(newDate) + โ””โ”€ fetchSlots() + +3. USER FILTERS PERIOD + โ”œโ”€ selectedPeriod.value = MORNING|AFTERNOON|EVENING|null + โ””โ”€ filteredSlots computed updates + โ””โ”€ SlotCards re-render (no new API call) + +4. USER PULLS TO REFRESH + โ”œโ”€ onRefresh() + โ””โ”€ loadSlots(selectedDate.value) + +5. USER TAPS "ๅฏ้ข„็บฆ" BUTTON + โ”œโ”€ onBookTap(slot) + โ”œโ”€ Check login (if not โ†’ login modal) + โ”œโ”€ Check membership (if not โ†’ purchase modal) + โ””โ”€ Show BookingConfirmPopup + โ””โ”€ Pre-select first membership + +6. USER CONFIRMS BOOKING + โ”œโ”€ onConfirmBooking({timeSlotId, membershipId}) + โ”œโ”€ POST /booking + โ”‚ โ””โ”€ bookingStore.createBooking() + โ”œโ”€ Show success toast + โ””โ”€ loadSlots(selectedDate.value) // Refresh + โ””โ”€ Updated slot.isBookedByMe = true + +7. USER TAPS "ๅ–ๆถˆ" BUTTON + โ”œโ”€ onCancelTap(slot) + โ”œโ”€ Confirm modal + โ”œโ”€ PUT /booking/:id/cancel + โ”‚ โ””โ”€ bookingStore.cancelBooking() + โ”œโ”€ Show success toast + โ””โ”€ loadSlots(selectedDate.value) // Refresh + โ””โ”€ Updated slot.isBookedByMe = false +``` + +--- + +## ๐Ÿงช Testing Scenarios + +### โœ… Happy Path +- [ ] Load page โ†’ today's slots display +- [ ] Tap date โ†’ slots for that date display +- [ ] Filter by period โ†’ slots filtered correctly +- [ ] Tap "ๅฏ้ข„็บฆ" โ†’ popup shows with correct time/date +- [ ] Select membership โ†’ deduction message updates +- [ ] Confirm โ†’ booking created, slot shows "ๅทฒ้ข„็บฆ" +- [ ] Pull to refresh โ†’ slots reload +- [ ] Tap "ๅ–ๆถˆ" โ†’ booking cancelled, slot back to "ๅฏ้ข„็บฆ" + +### โš ๏ธ Edge Cases +- [ ] No slots for date โ†’ empty state appears +- [ ] User not logged in โ†’ login modal shows +- [ ] No valid membership โ†’ purchase modal shows +- [ ] Network error โ†’ ??? (currently shows empty) +- [ ] Slot changes to FULL โ†’ button becomes disabled +- [ ] Slot changes to CLOSED โ†’ button becomes disabled + +--- + +## ๐Ÿ”ง Integration Points + +**From Backend:** +1. โœ… GET `/time-slot/available?date=...` โ†’ Returns slots +2. โœ… POST `/booking` โ†’ Create booking +3. โœ… PUT `/booking/:id/cancel` โ†’ Cancel booking +4. โœ… GET `/membership/my` โ†’ List memberships +5. โœ… Auth via Bearer token + +**From Frontend:** +1. โœ… LocalStorage for token persistence +2. โœ… uni.showModal, uni.showToast for UI feedback +3. โœ… uni.getSystemInfoSync() for responsive sizing +4. โœ… uni.navigateTo() for page navigation + +--- + +## ๐Ÿ“ฑ Responsive Layout + +**Design Breakpoint:** +- Base: 750rpx (WeChat standard width unit) +- Window height: dynamic via uni.getSystemInfoSync().windowHeight + +**Scroll Area Height Calculation:** +```typescript +scrollHeight = windowHeight - headerHeight(220rpx) - tabbarHeight(100rpx) + = windowHeight - (220 * (windowWidth / 750)) - (100 * (windowWidth / 750)) +``` + +**Sticky Header:** +- Position: sticky (CSS) +- Top: 0 +- Z-index: 100 +- Contains: DateSelector + TimePeriodFilter + +--- + +## ๐ŸŽฏ Summary + +The booking system is well-architected with: +- โœ… Clear separation of concerns (component, store, utils) +- โœ… Proper type safety with TypeScript +- โœ… Responsive date/time selection +- โœ… Membership-based booking validation +- โœ… Optimistic loading states +- โœ… Accessible UI patterns + +But needs: +- โš ๏ธ Better error handling +- โš ๏ธ Fix GET request implementation +- โš ๏ธ Loading state during date/period changes +- โš ๏ธ Network error user feedback + diff --git a/BOOKING_README.md b/BOOKING_README.md new file mode 100644 index 0000000..e4ca48b --- /dev/null +++ b/BOOKING_README.md @@ -0,0 +1,395 @@ +# Booking Page Documentation + +## ๐Ÿ“š Overview + +This folder contains comprehensive documentation for the WeChat Mini-Program booking system in the mp-pilates project (Uni-app + Vue 3). + +### ๐Ÿ“„ Documentation Files + +1. **BOOKING_PAGE_ANALYSIS.md** โญ START HERE + - Complete file-by-file breakdown of all components + - Data flow diagrams + - API contract documentation + - Color scheme and styling details + - Potential issues and problems + +2. **COMPONENT_HIERARCHY.md** + - Visual component tree structure + - State management flow (Pinia stores) + - API sequence diagrams + - State machine for slot cards + - Data transformations + +3. **QUICK_REFERENCE.md** + - Code snippets for quick lookup + - Debugging tips and console commands + - Common issues and solutions + - Debugging checklist + - API examples + +--- + +## ๐ŸŽฏ Quick Navigation + +### I want to understand... + +**...the overall flow** +โ†’ Read: BOOKING_PAGE_ANALYSIS.md โ†’ "Complete Data Flow Diagram" section + +**...how the UI is structured** +โ†’ Read: COMPONENT_HIERARCHY.md โ†’ "Component Tree" + "UI Layout Breakdown" + +**...where specific code is** +โ†’ Read: QUICK_REFERENCE.md โ†’ "Finding Specific Things" + +**...how to debug an issue** +โ†’ Read: QUICK_REFERENCE.md โ†’ "Common Issues & Solutions" + +**...the API contracts** +โ†’ Read: QUICK_REFERENCE.md โ†’ "API Contract Summary" + +**...the store state** +โ†’ Read: COMPONENT_HIERARCHY.md โ†’ "State Management Flow" + +--- + +## ๐Ÿ—๏ธ Project Structure + +``` +packages/app/src/ +โ”œโ”€โ”€ pages/ +โ”‚ โ””โ”€โ”€ booking/ +โ”‚ โ””โ”€โ”€ index.vue # Main booking page (311 lines) +โ”œโ”€โ”€ components/ +โ”‚ โ”œโ”€โ”€ DateSelector.vue # Date picker (50 lines) +โ”‚ โ”œโ”€โ”€ TimePeriodFilter.vue # Time period filter (50 lines) +โ”‚ โ”œโ”€โ”€ SlotCard.vue # Individual slot card (230 lines) +โ”‚ โ””โ”€โ”€ BookingConfirmPopup.vue # Booking confirmation modal (430 lines) +โ”œโ”€โ”€ stores/ +โ”‚ โ”œโ”€โ”€ booking.ts # Booking state (72 lines) +โ”‚ โ””โ”€โ”€ user.ts # User/membership state (110 lines) +โ””โ”€โ”€ utils/ + โ”œโ”€โ”€ request.ts # API request utilities (80 lines) + โ””โ”€โ”€ format.ts # Date formatting utilities (50 lines) + +packages/shared/src/ +โ”œโ”€โ”€ types/ +โ”‚ โ”œโ”€โ”€ time-slot.ts # TimeSlot types +โ”‚ โ”œโ”€โ”€ api.ts # API response types +โ”‚ โ””โ”€โ”€ booking.ts # Booking types +โ”œโ”€โ”€ constants.ts # TIME_PERIODS, etc +โ””โ”€โ”€ enums.ts # Enums (TimeSlotStatus, etc) +``` + +--- + +## ๐Ÿ”„ Data Flow at a Glance + +``` +Page Load + โ†“ +[Check login + load memberships] + โ†“ +Store: fetchSlots(today) + โ†“ +API: GET /time-slot/available?date=TODAY + โ†“ +State: bookingStore.slots = [TimeSlotWithBookingStatus[], ...] + โ†“ +Computed: filteredSlots (optionally filtered by period) + โ†“ +Render: SlotCard components + โ†“ +User interaction: + - Tap date โ†’ loadSlots(newDate) + - Filter period โ†’ filteredSlots re-computed + - Book slot โ†’ onBookTap() โ†’ popup + - Confirm โ†’ createBooking() โ†’ refresh slots + - Cancel โ†’ cancelBooking() โ†’ refresh slots +``` + +--- + +## ๐ŸŽญ Key Components + +### 1. pages/booking/index.vue +**Role:** Main page that orchestrates everything +**State:** selectedDate, selectedPeriod, showConfirmPopup, pendingSlot +**Stores:** bookingStore, userStore +**Key computed:** scrollHeight, filteredSlots + +### 2. components/SlotCard.vue +**Role:** Displays individual time slot +**Props:** slot (TimeSlotWithBookingStatus) +**Emits:** book, cancel +**States:** 4 button states based on status + isBookedByMe + +### 3. components/DateSelector.vue +**Role:** Horizontal date picker +**Props:** modelValue (YYYY-MM-DD) +**Data:** dateRange (7 days from today) +**Display:** Shows weekday, day number, month + +### 4. components/TimePeriodFilter.vue +**Role:** Horizontal tab filter +**Props:** modelValue (MORNING|AFTERNOON|EVENING|null) +**Constants:** TIME_PERIODS from shared + +### 5. components/BookingConfirmPopup.vue +**Role:** Modal for confirming booking +**Props:** visible, slot, memberships +**State:** selectedMembershipId (auto-selected on show) +**Logic:** Auto-select first membership when popup opens + +### 6. stores/booking.ts +**Actions:** +- fetchSlots(date) โ†’ GET /time-slot/available?date= +- createBooking(dto) โ†’ POST /booking +- cancelBooking(bookingId) โ†’ PUT /booking/:id/cancel +- fetchMyBookings(status?) โ†’ GET /booking/my +- fetchUpcomingBookings() โ†’ GET /booking/my/upcoming + +### 7. stores/user.ts +**Computed:** +- loggedIn: !!token.value +- hasValidMembership: activeMemberships.length > 0 +- activeMemberships: memberships filtered by ACTIVE status + +--- + +## ๐Ÿ“Š State Types + +### TimeSlotWithBookingStatus +```typescript +{ + id: string // UUID + date: "2026-04-05" // YYYY-MM-DD + startTime: "09:00" // HH:MM + endTime: "10:00" // HH:MM + capacity: 1 // Max slots + bookedCount: 0 // Currently booked + status: "OPEN" | "FULL" | "CLOSED" + source: "MANUAL" | "TEMPLATE" + templateId: null + isBookedByMe: boolean // User has booked this + myBookingId: string | null // Booking ID (for cancel) +} +``` + +### MembershipWithCardType +```typescript +{ + id: string + cardType: { name: string, ... } + status: "ACTIVE" | "EXPIRED" | "USED_UP" + remainingTimes: number | null + expireDate: "2026-12-31" +} +``` + +--- + +## ๐ŸŽจ Visual States + +### Slot Card Button States + +| Condition | Button | Color | Action | +|-----------|--------|-------|--------| +| OPEN, not booked | "ๅฏ้ข„็บฆ" | Tan (#c9a87c) | Show popup | +| OPEN, booked by me | "ๅทฒ้ข„็บฆ" + "ๅ–ๆถˆ" link | Tan + Red | Show cancel confirm | +| FULL | "ๅทฒ็บฆๆปก" | Gray (#f0f0f0) | Disabled | +| CLOSED | "ๅทฒๅ…ณ้—ญ" | Gray (#f0f0f0) | Disabled | + +### Capacity Badge Colors + +| Condition | Background | Text | Meaning | +|-----------|------------|------|---------| +| <80% booked | #f0faf3 | #4caf50 | Green - Plenty of spots | +| โ‰ฅ80% booked | #fff8ed | #f59e0b | Orange - Almost full | +| FULL | #fef0f0 | #ef4444 | Red - No spots | +| CLOSED | #f5f5f5 | #999 | Gray - Unavailable | + +--- + +## ๐Ÿ” Authentication + +- Token stored in localStorage +- Automatically included in request headers +- 401 response โ†’ Clear token + show "please login" toast +- onBookTap checks loggedIn โ†’ shows login modal if needed +- onBookTap checks hasValidMembership โ†’ shows purchase modal if needed + +--- + +## ๐Ÿ“ก API Endpoints + +### GET /time-slot/available?date=YYYY-MM-DD +``` +Query: date (required, YYYY-MM-DD format) +Returns: TimeSlotWithBookingStatus[] +Auth: Bearer token required +``` + +### POST /booking +``` +Body: { timeSlotId, membershipId } +Returns: BookingWithDetails +Auth: Bearer token required +``` + +### PUT /booking/:bookingId/cancel +``` +Path: bookingId +Returns: BookingWithDetails (with status: CANCELLED) +Auth: Bearer token required +``` + +### GET /membership/my +``` +Returns: MembershipWithCardType[] +Auth: Bearer token required +``` + +--- + +## โš ๏ธ Known Issues + +### 1. GET Request Body Issue +- File: `utils/request.ts`, `get()` function +- Problem: Data passed as body instead of query params +- Impact: Might not work on all platforms + +### 2. Error Handling +- File: `stores/booking.ts`, `fetchSlots()` +- Problem: Network error โ†’ empty array instead of error message +- Impact: Users can't tell if error or truly no slots + +### 3. Loading State +- File: `pages/booking/index.vue` +- Problem: Skeleton only appears on initial load +- Impact: Date changes appear instant (confusing on slow network) + +### 4. Date Math +- File: `utils/format.ts`, `getDateRange()` +- Problem: Uses ms arithmetic (86400000ms per day) +- Impact: Doesn't account for DST transitions + +--- + +## ๐Ÿงช Testing Checklist + +### Happy Path +- [ ] Load page โ†’ today's slots display +- [ ] Tap date โ†’ slots change for that date +- [ ] Filter by period โ†’ slots filtered correctly +- [ ] Tap "ๅฏ้ข„็บฆ" โ†’ popup shows +- [ ] Confirm booking โ†’ slot shows "ๅทฒ้ข„็บฆ" +- [ ] Tap "ๅ–ๆถˆ" โ†’ booking cancelled, slot resets +- [ ] Pull to refresh โ†’ slots reload + +### Edge Cases +- [ ] No slots for date โ†’ empty state appears +- [ ] Not logged in โ†’ login modal on book tap +- [ ] No valid membership โ†’ purchase modal on book tap +- [ ] Network error โ†’ ??? (currently shows empty) +- [ ] Slot becomes FULL โ†’ button updates to disabled +- [ ] Multiple memberships โ†’ can select different card + +--- + +## ๐Ÿ“ File Sizes + +| File | Lines | Purpose | +|------|-------|---------| +| pages/booking/index.vue | 311 | Main page orchestration | +| components/BookingConfirmPopup.vue | 430 | Booking modal | +| components/SlotCard.vue | 230 | Slot display | +| stores/booking.ts | 72 | Booking state | +| utils/request.ts | 80 | API client | +| components/DateSelector.vue | 50 | Date picker | +| components/TimePeriodFilter.vue | 50 | Period filter | +| utils/format.ts | 50 | Date utilities | + +--- + +## ๐ŸŽ“ Learning Path + +**Level 1: Overview** +1. Read this file +2. Look at BOOKING_PAGE_ANALYSIS.md โ†’ "Complete Data Flow Diagram" + +**Level 2: Components** +1. Read COMPONENT_HIERARCHY.md โ†’ "Component Tree" +2. Read BOOKING_PAGE_ANALYSIS.md โ†’ "File-by-File Analysis" + +**Level 3: Implementation** +1. Read QUICK_REFERENCE.md โ†’ "Where Slots Come From" +2. Read actual source files in order: + - stores/booking.ts + - pages/booking/index.vue + - components/SlotCard.vue + - components/BookingConfirmPopup.vue + +**Level 4: Debugging** +1. Read QUICK_REFERENCE.md โ†’ "Debugging Tips" +2. Read QUICK_REFERENCE.md โ†’ "Common Issues & Solutions" + +**Level 5: Deep Dive** +1. Read COMPONENT_HIERARCHY.md โ†’ "State Management Flow" +2. Read COMPONENT_HIERARCHY.md โ†’ "API Calls Sequence" +3. Study utils/request.ts for request handling + +--- + +## ๐Ÿ”— Related Documentation + +- Backend: `/packages/server/src/time-slot/` +- Shared types: `/packages/shared/src/types/` +- Auth: `/packages/app/src/utils/auth.ts` +- User store: `/packages/app/src/stores/user.ts` + +--- + +## ๐Ÿ“ž Quick Answers + +**Q: Why doesn't the page load?** +A: Check 1) Is API returning data? 2) Is token valid? 3) Check console for errors + +**Q: Why doesn't filtering work?** +A: Check 1) Is selectedPeriod.value being set? 2) Is slot.startTime correct format? + +**Q: Why doesn't the booking button work?** +A: Check 1) Is slot.status === OPEN? 2) Is isBookedByMe === false? 3) Is user logged in? + +**Q: How do I add error handling?** +A: See QUICK_REFERENCE.md โ†’ "Issue 1: Slots not loading" โ†’ Solution + +**Q: How do I test the booking flow?** +A: See "Testing Checklist" section above + +--- + +## ๐Ÿš€ Common Tasks + +### Add loading indicator during date change +โ†’ Use bookingStore.loadingSlots in template + +### Show error message for API failures +โ†’ Add error state to bookingStore, show in template + +### Change colors/styling +โ†’ Edit style blocks in .vue files (see color scheme in BOOKING_PAGE_ANALYSIS.md) + +### Modify time period ranges +โ†’ Edit TIME_PERIODS in packages/shared/src/constants.ts + +### Change initial date or time range +โ†’ Edit pages/booking/index.vue onMounted() or DATE_SELECTOR_DAYS constant + +### Add/remove date selector days +โ†’ Edit DATE_SELECTOR_DAYS in packages/shared/src/constants.ts + +--- + +Generated: 2026-04-05 +Last Updated: BOOKING_PAGE_ANALYSIS.md diff --git a/COMPONENT_HIERARCHY.md b/COMPONENT_HIERARCHY.md new file mode 100644 index 0000000..529a107 --- /dev/null +++ b/COMPONENT_HIERARCHY.md @@ -0,0 +1,359 @@ +# 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 +``` + diff --git a/QUICK_REFERENCE.md b/QUICK_REFERENCE.md new file mode 100644 index 0000000..85f9664 --- /dev/null +++ b/QUICK_REFERENCE.md @@ -0,0 +1,592 @@ +# Booking Page - Quick Reference & Code Snippets + +## ๐Ÿš€ Quick Start: Understanding the Flow + +### Where Slots Come From +```typescript +// 1. Store calls API +packages/app/src/stores/booking.ts:17-27 +async function fetchSlots(date: string) { + loadingSlots.value = true + try { + // GET /time-slot/available?date=2026-04-05 + slots.value = await get( + '/time-slot/available', + { date } + ) + } catch (err) { + console.error('Fetch slots failed:', err) + slots.value = [] // โš ๏ธ Clears on error! + } finally { + loadingSlots.value = false + } +} +``` + +### Where Time Periods Are Defined +```typescript +// packages/shared/src/constants.ts:11-15 +export const TIME_PERIODS = { + MORNING: { label: 'ไธŠๅˆ', start: '06:00', end: '12:00' }, + AFTERNOON: { label: 'ไธ‹ๅˆ', start: '12:00', end: '18:00' }, + EVENING: { label: 'ๆ™šไธŠ', start: '18:00', end: '22:00' }, +} as const +``` + +### Where Filtering Happens +```typescript +// pages/booking/index.vue:94-103 +const filteredSlots = computed(() => { + const slots = bookingStore.slots as TimeSlotWithBookingStatus[] + if (!selectedPeriod.value) return [...slots] + + const period = TIME_PERIODS[selectedPeriod.value] + return slots.filter((slot) => { + const t = slot.startTime // "09:00", "10:00", etc + return t >= period.start && t < period.end + }) +}) +``` + +### Slot Rendering +```vue + + + + +``` + +--- + +## ๐Ÿ” Finding Specific Things + +### Q: Where do the time slot types come from? +**A:** `packages/shared/src/types/time-slot.ts` +```typescript +interface TimeSlotWithBookingStatus extends TimeSlot { + readonly isBookedByMe: boolean // true if user booked it + readonly myBookingId: string | null // needed for cancellation +} + +interface TimeSlot { + readonly id: string // UUID + readonly date: string // "2026-04-05" + readonly startTime: string // "09:00" + readonly endTime: string // "10:00" + readonly capacity: number // 1 (for private lessons) + readonly bookedCount: number // 0 or 1 + readonly status: TimeSlotStatus // OPEN|FULL|CLOSED + readonly source: TimeSlotSource // TEMPLATE|MANUAL + readonly templateId: string | null + readonly createdAt: string + readonly updatedAt: string +} +``` + +### Q: Where is the membership selection happening? +**A:** `components/BookingConfirmPopup.vue:136-147` +```typescript +const selectedMembershipId = ref('') + +watch( + [() => props.visible, () => props.memberships], + ([visible, memberships]) => { + if (visible && memberships.length > 0) { + selectedMembershipId.value = memberships[0].id // Auto-select first + } + }, + { immediate: true }, +) +``` + +### Q: Where are the button states determined? +**A:** `components/SlotCard.vue:15-45` +```vue + + + + + + + + +``` + +### Q: Where is the API request actually made? +**A:** `utils/request.ts:22-59` +```typescript +export function request(options: RequestOptions): Promise { + return new Promise((resolve, reject) => { + const token = uni.getStorageSync('token') as string + + uni.request({ + url: `${BASE_URL}${options.url}`, // BASE_URL = http://localhost:3000/api + method: options.method || 'GET', + data: options.data, + header: { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...options.header, + }, + success: (res) => { + if (res.statusCode === 401) { + uni.removeStorageSync('token') + uni.showToast({ title: '่ฏท้‡ๆ–ฐ็™ปๅฝ•', icon: 'none' }) + reject(new Error('Unauthorized')) + return + } + if (res.statusCode >= 400) { + const body = res.data as ApiResponse + reject(new Error(body?.message || `่ฏทๆฑ‚ๅคฑ่ดฅ (${res.statusCode})`)) + return + } + const body = res.data as ApiResponse + if (body.success) { + resolve(body.data as T) // โ† Extract data from ApiResponse + } else { + reject(new Error(body.message || '่ฏทๆฑ‚ๅคฑ่ดฅ')) + } + }, + fail: (err) => { + reject(new Error(err.errMsg || '็ฝ‘็ปœ่ฏทๆฑ‚ๅคฑ่ดฅ')) + }, + }) + }) +} +``` + +--- + +## ๐Ÿ› Debugging Tips + +### Tip 1: Check what's in the store +```typescript +// In browser console while in booking page: +console.log('Slots:', JSON.stringify(uni.$u.pinia.state.value.booking.slots, null, 2)) +console.log('Selected period:', uni.$u.pinia.state.value.booking.selectedPeriod) +``` + +### Tip 2: Log slot filtering +```typescript +// Add to pages/booking/index.vue filteredSlots computed: +const filteredSlots = computed(() => { + const slots = bookingStore.slots as TimeSlotWithBookingStatus[] + if (!selectedPeriod.value) { + console.log('No period filter, showing all slots:', slots.length) + return [...slots] + } + + const period = TIME_PERIODS[selectedPeriod.value] + console.log(`Filtering by ${selectedPeriod.value}:`, period) + console.log('All slot times:', slots.map(s => s.startTime)) + + const filtered = slots.filter((slot) => { + const t = slot.startTime + const matches = t >= period.start && t < period.end + if (!matches) console.log(`${t} not in [${period.start}, ${period.end})`) + return matches + }) + + console.log('Filtered result:', filtered.length) + return filtered +}) +``` + +### Tip 3: Verify API response +```typescript +// In stores/booking.ts fetchSlots(): +async function fetchSlots(date: string) { + loadingSlots.value = true + try { + console.log('Fetching slots for date:', date) + slots.value = await get( + '/time-slot/available', + { date } + ) + console.log('Received slots:', slots.value) + console.log('Slot count:', slots.value.length) + if (slots.value.length > 0) { + console.log('First slot:', JSON.stringify(slots.value[0], null, 2)) + } + } catch (err) { + console.error('Fetch slots failed:', err) + slots.value = [] + } finally { + loadingSlots.value = false + } +} +``` + +### Tip 4: Check network requests +```typescript +// Open WeChat DevTools โ†’ Network tab +// Look for GET request to /time-slot/available +// Check: +// โœ“ URL has ?date=YYYY-MM-DD +// โœ“ Authorization header exists +// โœ“ Response status 200 +// โœ“ Response body has "success": true +``` + +--- + +## โŒ Common Issues & Solutions + +### Issue 1: Slots not loading +**Symptoms:** +- Page shows "ๅฝ“ๆ—ฅๆš‚ๆ— ๅฏ็บฆๆ—ถๆฎต" (no slots) +- No error message + +**Check list:** +```typescript +// 1. Is API endpoint correct? +// Check: /time-slot/available?date=2026-04-05 +// Should return TimeSlotWithBookingStatus[] + +// 2. Is date format correct? +// Page sends: formatDate(new Date()) โ†’ "2026-04-05" +// API expects: "YYYY-MM-DD" +console.log(formatDate(new Date())) // Should output: "2026-04-05" + +// 3. Is authentication working? +console.log('Token:', uni.getStorageSync('token')) + +// 4. Check for errors in console +// If fetchSlots fails, slots.value becomes [] +``` + +**Solution:** +```typescript +// In bookingStore.fetchSlots(), add error state: +const error = ref(null) + +async function fetchSlots(date: string) { + loadingSlots.value = true + error.value = null // Clear previous error + try { + slots.value = await get( + '/time-slot/available', + { date } + ) + } catch (err) { + console.error('Fetch slots failed:', err) + error.value = err instanceof Error ? err.message : 'ๅŠ ่ฝฝๅคฑ่ดฅ' + slots.value = [] + } finally { + loadingSlots.value = false + } +} + +// Then in page template: + + {{ error }} + ้‡่ฏ• + +``` + +### Issue 2: Time period filtering not working +**Symptoms:** +- Select "ไธŠๅˆ" (morning) but all slots still show +- Or vice versa + +**Check:** +```typescript +// 1. Verify TIME_PERIODS constant +console.log('TIME_PERIODS:', TIME_PERIODS) + +// 2. Check selectedPeriod value +console.log('Selected period:', selectedPeriod.value) + +// 3. Verify slot.startTime format +// Should be "HH:MM" like "09:00", not "09:00:00" +bookingStore.slots.forEach(slot => { + console.log('Slot time:', slot.startTime, 'format ok?', /^\d{2}:\d{2}$/.test(slot.startTime)) +}) + +// 4. Test filtering manually +const slot = bookingStore.slots[0] +const period = TIME_PERIODS.MORNING +console.log(`${slot.startTime} >= ${period.start}?`, slot.startTime >= period.start) +console.log(`${slot.startTime} < ${period.end}?`, slot.startTime < period.end) +``` + +**Solution:** +```typescript +// If time format is "09:00:00", slice it: +const filteredSlots = computed(() => { + const slots = bookingStore.slots as TimeSlotWithBookingStatus[] + if (!selectedPeriod.value) return [...slots] + + const period = TIME_PERIODS[selectedPeriod.value] + return slots.filter((slot) => { + // Ensure HH:MM format + const t = slot.startTime.slice(0, 5) // "09:00:00" โ†’ "09:00" + return t >= period.start && t < period.end + }) +}) +``` + +### Issue 3: Booking button not responding +**Symptoms:** +- Click "ๅฏ้ข„็บฆ" but nothing happens +- No modal appears + +**Check:** +```typescript +// 1. Is slot.status correct? +console.log('Slot status:', slot.status) +// Should be "OPEN" to show book button + +// 2. Is isBookedByMe false? +console.log('Is booked by me?', slot.isBookedByMe) +// Should be false to show book button + +// 3. Is onBookTap being called? +// Add to pages/booking/index.vue: +async function onBookTap(slot: TimeSlotWithBookingStatus) { + console.log('Book tapped for slot:', slot) // โ† Should log + + // Rest of code... +} + +// 4. Is userStore.loggedIn true? +console.log('Logged in?', userStore.loggedIn) +``` + +### Issue 4: Membership not showing in popup +**Symptoms:** +- Booking popup appears but no membership card shown +- "ๆš‚ๆ— ๅฏ็”จไผšๅ‘˜ๅก" displayed + +**Check:** +```typescript +// 1. Are memberships loaded? +console.log('Memberships:', userStore.memberships) + +// 2. Are any memberships ACTIVE? +console.log('Active memberships:', userStore.activeMemberships) +console.log('Has valid membership?', userStore.hasValidMembership) + +// 3. Are memberships passed to popup? +// In pages/booking/index.vue: + +console.log('Popup passed memberships:', userStore.activeMemberships) +``` + +**Solution:** +```typescript +// In onMounted: +onMounted(async () => { + if (userStore.loggedIn && userStore.activeMemberships.length === 0) { + console.log('Fetching memberships...') + try { + await userStore.fetchMemberships() + console.log('Memberships loaded:', userStore.activeMemberships) + } catch (err) { + console.error('Failed to fetch memberships:', err) + uni.showToast({ title: 'ๅŠ ่ฝฝไผšๅ‘˜ๅกๅคฑ่ดฅ', icon: 'none' }) + } + } + await loadSlots(selectedDate.value) +}) +``` + +--- + +## ๐Ÿ“Š Capacity Display Logic + +### How Capacity Color is Determined +```typescript +// components/SlotCard.vue:69-81 +const capacityLabel = computed(() => { + const { bookedCount, capacity, status } = props.slot + if (status === TimeSlotStatus.CLOSED) return 'ๅทฒๅ…ณ้—ญ' + return `${bookedCount}/${capacity} ไบบ` +}) + +const capacityClass = computed(() => { + const { bookedCount, capacity, status } = props.slot + if (status === TimeSlotStatus.CLOSED) return 'cap-closed' + if (status === TimeSlotStatus.FULL) return 'cap-full' + if (bookedCount >= capacity * 0.8) return 'cap-almost' + return 'cap-open' +}) + +// Color mapping in styles: +// cap-open: #f0faf3 bg, #4caf50 text (green) - <80% booked +// cap-almost: #fff8ed bg, #f59e0b text (orange) - โ‰ฅ80% booked +// cap-full: #fef0f0 bg, #ef4444 text (red) - status: FULL +// cap-closed: #f5f5f5 bg, #999 text (gray) - status: CLOSED +``` + +### Example Calculations +```typescript +// Slot 1: capacity=1, bookedCount=0, status=OPEN +// 0/1 ไบบ in green badge (0% booked) + +// Slot 2: capacity=1, bookedCount=1, status=OPEN +// 1/1 ไบบ in red badge (100% booked โ‰ฅ 80%) + +// Slot 3: capacity=5, bookedCount=4, status=OPEN +// 4/5 ไบบ in orange badge (80% booked โ‰ฅ 80%) + +// Slot 4: capacity=5, bookedCount=3, status=OPEN +// 3/5 ไบบ in green badge (60% booked < 80%) +``` + +--- + +## ๐Ÿ”— API Contract Summary + +### GET /time-slot/available + +**Request:** +``` +GET /api/time-slot/available?date=2026-04-05 +Authorization: Bearer +``` + +**Response (200 OK):** +```json +{ + "success": true, + "data": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "date": "2026-04-05", + "startTime": "09:00", + "endTime": "10:00", + "capacity": 1, + "bookedCount": 0, + "status": "OPEN", + "source": "MANUAL", + "templateId": null, + "createdAt": "2026-04-01T10:00:00Z", + "updatedAt": "2026-04-05T09:00:00Z", + "isBookedByMe": false, + "myBookingId": null + } + ], + "message": null +} +``` + +**Error (400):** +```json +{ + "success": false, + "data": null, + "message": "Invalid date format" +} +``` + +### POST /booking + +**Request:** +```json +POST /api/booking +{ + "timeSlotId": "550e8400-e29b-41d4-a716-446655440000", + "membershipId": "220e8400-e29b-41d4-a716-446655440111" +} +``` + +**Response (201):** +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440222", + "userId": "user-123", + "timeSlotId": "550e8400-e29b-41d4-a716-446655440000", + "membershipId": "220e8400-e29b-41d4-a716-446655440111", + "status": "CONFIRMED", + "bookedAt": "2026-04-05T10:30:00Z", + "courseDate": "2026-04-05", + "courseTime": "09:00", + "instructorName": "instructor name", + "isCompleted": false + }, + "message": null +} +``` + +### PUT /booking/:id/cancel + +**Request:** +``` +PUT /api/booking/550e8400-e29b-41d4-a716-446655440222/cancel +``` + +**Response (200):** +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440222", + "status": "CANCELLED", + "cancelledAt": "2026-04-05T10:35:00Z" + }, + "message": null +} +``` + +--- + +## ๐ŸŽฏ Next Steps for Debugging + +1. **Verify API Endpoint** + - Open DevTools โ†’ Network + - Check `/time-slot/available?date=...` request + - Confirm response has `"success": true` + - Confirm data array is not empty + +2. **Check Store State** + - Add console.logs to bookingStore.fetchSlots() + - Verify slots are set correctly + - Check loadingSlots toggle + +3. **Verify Computed Properties** + - Log filteredSlots in component + - Check if filtering logic works + - Verify slot.startTime format + +4. **Test User Interaction** + - Click date item โ†’ verify onDateSelect fires + - Click period tab โ†’ verify onPeriodChange fires + - Click book button โ†’ verify onBookTap fires + - Check modals appear + +5. **Check Mobile-Specific Issues** + - Test in WeChat DevTools + - Check rpx calculations + - Verify touch events work + diff --git a/SCHEDULING_DOCUMENTATION_INDEX.md b/SCHEDULING_DOCUMENTATION_INDEX.md new file mode 100644 index 0000000..8deb002 --- /dev/null +++ b/SCHEDULING_DOCUMENTATION_INDEX.md @@ -0,0 +1,244 @@ +# WeChat Mini-Program Admin Scheduling - Documentation Index + +**Created**: 2026-04-05 +**Project**: mp-pilates (Pilates Studio Booking System) + +--- + +## ๐Ÿ“š Documentation Files + +This exploration contains **3 comprehensive documents** about the admin scheduling/ๆŽ’่ฏพ่ฎพ็ฝฎ system: + +### 1. **ADMIN_SCHEDULING_EXPLORATION.md** (24 KB, 803 lines) +**Purpose**: Complete deep-dive into the scheduling system + +**Sections**: +- Executive Summary +- File Structure (frontend, backend, shared) +- 4 Key Components (Admin Dashboard, Week Templates, Slot Adjustment, Admin Store) +- Backend Architecture (Controllers, Services, Slot Generator) +- Data Flow & User Journey +- Constants & Utilities +- Permission Model +- Implementation Status +- Edge Cases +- UI Design Patterns +- Deployment & Configuration + +**Best for**: Understanding the complete architecture and how everything connects + +--- + +### 2. **SCHEDULING_FLOW_DIAGRAM.md** (13 KB, 271 lines) +**Purpose**: Visual flowcharts and architecture diagrams + +**Sections**: +- Component Architecture (visual tree) +- Data Flow: Template โ†’ Slots (visual flowchart) +- State Management breakdown +- API Endpoints Summary +- Entity Relationships (ER diagram) +- Weekday Mapping (ISO vs JS conversion) +- Timeline Example (realistic scenario) + +**Best for**: Quick visual understanding of the flow and architecture + +--- + +### 3. **SCHEDULING_QUICK_REFERENCE.md** (7.9 KB, 296 lines) +**Purpose**: Quick lookup guide for developers + +**Sections**: +- Quick Links to Key Files (with line numbers) +- The Flow in 30 Seconds +- Core Entities (WeekTemplate, TimeSlot) +- API Endpoints (with JSON examples) +- UI State Management +- Permissions & Auth +- Important Constants +- Common Gotchas (5 key points) +- Usage Example (step-by-step) +- Related Components +- Scalability Notes + +**Best for**: Developers jumping into the code for the first time + +--- + +## ๐ŸŽฏ Choose Your Path + +### If you want to... + +**Understand the big picture** +โ†’ Read: `SCHEDULING_FLOW_DIAGRAM.md` +โ†’ Then: `ADMIN_SCHEDULING_EXPLORATION.md` (section 2) + +**Start coding immediately** +โ†’ Read: `SCHEDULING_QUICK_REFERENCE.md` +โ†’ Then: Jump to specific file links + +**Debug a specific issue** +โ†’ Read: `SCHEDULING_QUICK_REFERENCE.md` (Common Gotchas) +โ†’ Then: Search in `ADMIN_SCHEDULING_EXPLORATION.md` + +**Understand data flow** +โ†’ Read: `SCHEDULING_FLOW_DIAGRAM.md` (Data Flow section) +โ†’ Then: `ADMIN_SCHEDULING_EXPLORATION.md` (section 7: Data Flow) + +--- + +## ๐Ÿ”‘ Key Files by Role + +### Frontend Developer +**Must Read**: +- `SCHEDULING_QUICK_REFERENCE.md` โ†’ UI State Management +- `packages/app/src/pages/admin/week-template.vue` (500 lines) +- `packages/app/src/pages/admin/slot-adjust.vue` (428 lines) +- `packages/app/src/stores/admin.ts` (171 lines) + +### Backend Developer +**Must Read**: +- `SCHEDULING_QUICK_REFERENCE.md` โ†’ API Endpoints +- `packages/server/src/time-slot/time-slot.controller.ts` +- `packages/server/src/time-slot/slot-generator.service.ts` +- `packages/server/src/time-slot/time-slot.service.ts` + +### Full-Stack Developer +**Must Read**: All documentation files in order: +1. `SCHEDULING_QUICK_REFERENCE.md` (5 min) +2. `SCHEDULING_FLOW_DIAGRAM.md` (10 min) +3. `ADMIN_SCHEDULING_EXPLORATION.md` (20 min) + +--- + +## ๐ŸŽ“ Learning Timeline + +### Day 1: Orientation (30 minutes) +- Read: `SCHEDULING_QUICK_REFERENCE.md` section "The Flow: In 30 Seconds" +- Skim: `SCHEDULING_FLOW_DIAGRAM.md` + +### Day 2: Deep Dive (1-2 hours) +- Read: `SCHEDULING_FLOW_DIAGRAM.md` (entire) +- Read: `ADMIN_SCHEDULING_EXPLORATION.md` (sections 1-3) + +### Day 3: Implementation (ongoing) +- Refer to: `SCHEDULING_QUICK_REFERENCE.md` as needed +- Cross-reference: `ADMIN_SCHEDULING_EXPLORATION.md` sections 4-8 +- Check: Backend/Frontend specific sections + +--- + +## ๐Ÿ”— File Paths: Quick Lookup + +| Component | Path | Lines | +|-----------|------|-------| +| Admin Dashboard | `packages/app/src/pages/admin/index.vue` | 177 | +| **Week Templates** | `packages/app/src/pages/admin/week-template.vue` | 500 โญ | +| Slot Adjustment | `packages/app/src/pages/admin/slot-adjust.vue` | 428 | +| Admin Store | `packages/app/src/stores/admin.ts` | 171 | +| API Controller | `packages/server/src/time-slot/time-slot.controller.ts` | 92 | +| API Service | `packages/server/src/time-slot/time-slot.service.ts` | 142 | +| Slot Generator | `packages/server/src/time-slot/slot-generator.service.ts` | 172 | +| Types: Templates | `packages/shared/src/types/week-template.ts` | 19 | +| Types: Slots | `packages/shared/src/types/time-slot.ts` | 30 | +| Constants | `packages/shared/src/constants.ts` | 22 | +| Utilities | `packages/app/src/utils/format.ts` | 47 | + +โญ = Main scheduling component (ๆŽ’่ฏพ่ฎพ็ฝฎ) + +--- + +## ๐Ÿ“Š System Overview + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ADMIN SCHEDULING SYSTEM โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ Frontend (Vue 3 + TypeScript) โ”‚ +โ”‚ โ”œโ”€ week-template.vue (templates CRUD) โ”‚ +โ”‚ โ”œโ”€ slot-adjust.vue (manual operations) โ”‚ +โ”‚ โ””โ”€ admin.ts (Pinia store) โ”‚ +โ”‚ โ”‚ +โ”‚ Backend (NestJS + Prisma) โ”‚ +โ”‚ โ”œโ”€ time-slot.controller.ts (API routes) โ”‚ +โ”‚ โ”œโ”€ time-slot.service.ts (business logic) โ”‚ +โ”‚ โ””โ”€ slot-generator.service.ts (auto-generation) โ”‚ +โ”‚ โ”‚ +โ”‚ Database (PostgreSQL/MySQL) โ”‚ +โ”‚ โ”œโ”€ WeekTemplate (recurring schedule rules) โ”‚ +โ”‚ โ”œโ”€ TimeSlot (actual bookable slots) โ”‚ +โ”‚ โ””โ”€ Booking (user reservations) โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## ๐Ÿš€ Quick Start Checklist + +- [ ] Read `SCHEDULING_QUICK_REFERENCE.md` (5 min) +- [ ] Skim `SCHEDULING_FLOW_DIAGRAM.md` (5 min) +- [ ] Open `packages/app/src/pages/admin/week-template.vue` +- [ ] Open `packages/server/src/time-slot/slot-generator.service.ts` +- [ ] Bookmark this index file for reference +- [ ] Ask questions about specific sections in the docs + +--- + +## ๐Ÿ“ Terms & Definitions + +| Term | Definition | +|------|-----------| +| **WeekTemplate** | Recurring schedule rule (e.g., "every Monday 9-10 AM") | +| **TimeSlot** | Actual bookable time (e.g., "Monday, April 6, 9-10 AM") | +| **ๆŽ’่ฏพ่ฎพ็ฝฎ** | Schedule setup (admin template management) | +| **ไธดๆ—ถ่ฐƒๆ•ด** | Temporary adjustments (manual slot operations) | +| **isDirty** | Flag indicating unsaved changes | +| **Atomic** | All-or-nothing database transaction | +| **skipDuplicates** | Prisma option to ignore duplicate records on batch insert | +| **ISO Weekday** | 1=Monday, 2=Tuesday, ..., 7=Sunday | + +--- + +## ๐Ÿ†˜ Getting Help + +### Question Type โ†’ Documentation + +**"How does admin add a new class?"** +โ†’ `SCHEDULING_QUICK_REFERENCE.md` โ†’ Usage Example + +**"What API endpoints exist?"** +โ†’ `SCHEDULING_QUICK_REFERENCE.md` โ†’ API Endpoints +โ†’ OR `ADMIN_SCHEDULING_EXPLORATION.md` โ†’ Backend Architecture + +**"How do templates become slots?"** +โ†’ `SCHEDULING_FLOW_DIAGRAM.md` โ†’ Data Flow section + +**"What database schema?"** +โ†’ `SCHEDULING_QUICK_REFERENCE.md` โ†’ Core Entities +โ†’ OR `SCHEDULING_FLOW_DIAGRAM.md` โ†’ Entity Relationships + +**"Where does X file?"** +โ†’ `SCHEDULING_QUICK_REFERENCE.md` โ†’ File Paths lookup table + +--- + +## โœ… Verification Checklist + +- [x] All 3 documentation files created +- [x] 803 + 271 + 296 = 1,370 lines of documentation +- [x] Complete file paths documented +- [x] API endpoints listed with examples +- [x] Data flow diagrams included +- [x] Common gotchas documented +- [x] Usage examples provided +- [x] Scalability notes included +- [x] Permission model explained +- [x] Timezone handling noted + +--- + +**Last Updated**: 2026-04-05 +**Status**: Complete and ready for reference + diff --git a/SCHEDULING_FLOW_DIAGRAM.md b/SCHEDULING_FLOW_DIAGRAM.md new file mode 100644 index 0000000..48fc79f --- /dev/null +++ b/SCHEDULING_FLOW_DIAGRAM.md @@ -0,0 +1,271 @@ +# Admin Scheduling Flow Diagram + +## Component Architecture + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Admin Dashboard โ”‚ +โ”‚ (pages/admin/index.vue) โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“… ๆŽ’่ฏพ่ฎพ็ฝฎ ๐Ÿ”ง ไธดๆ—ถ่ฐƒๆ•ด ๐Ÿ‘ฅ ไผšๅ‘˜ ๐Ÿ“‹ ่ฎขๅ• ๐Ÿ’ณ ๅก ๐Ÿข ๅทฅไฝœๅฎค +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ””โ”€โ–บ ๐Ÿ“… ๆŽ’่ฏพ่ฎพ็ฝฎ (Week Template) + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ pages/admin/week-template.vue โ”‚ + โ”‚ ================================ โ”‚ + โ”‚ โ”‚ + โ”‚ 1. Fetch Templates (onMounted) โ”‚ + โ”‚ โ””โ”€ GET /admin/week-template โ”‚ + โ”‚ โ”‚ + โ”‚ 2. Display grouped by day (Mon-Sun) โ”‚ + โ”‚ โ”‚ + โ”‚ 3. Add/Edit/Delete/Toggle locally โ”‚ + โ”‚ โ””โ”€ isDirty flag = true โ”‚ + โ”‚ โ”‚ + โ”‚ 4. Save All Changes (bottom bar) โ”‚ + โ”‚ โ””โ”€ PUT /admin/week-template โ”‚ + โ”‚ (Full template array) โ”‚ + โ”‚ โ”‚ + โ”‚ 5. Backend transaction: โ”‚ + โ”‚ - DELETE all templates โ”‚ + โ”‚ - CREATE new templates โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + โ””โ”€โ–บ ๐Ÿ”ง ไธดๆ—ถ่ฐƒๆ•ด (Slot Adjustment - 3 Tabs) + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ pages/admin/slot-adjust.vue โ”‚ + โ”‚ ================================ โ”‚ + โ”‚ โ”‚ + โ”‚ TAB 0: ๆ–ฐๅขžๆ—ถๆฎต (Add Manual Slot) โ”‚ + โ”‚ โ”œโ”€ Date picker โ”‚ + โ”‚ โ”œโ”€ Time pickers โ”‚ + โ”‚ โ”œโ”€ Capacity input โ”‚ + โ”‚ โ””โ”€ POST /admin/time-slot/manual โ”‚ + โ”‚ โ””โ”€ Creates slot with source=MANUAL โ”‚ + โ”‚ โ”‚ + โ”‚ TAB 1: ๅ…ณ้—ญๆ—ถๆฎต (Close Slots) โ”‚ + โ”‚ โ”œโ”€ Date picker โ”‚ + โ”‚ โ”œโ”€ Fetch slots for date โ”‚ + โ”‚ โ”‚ โ””โ”€ GET /admin/time-slots?date=XXX โ”‚ + โ”‚ โ”œโ”€ Display with status badges โ”‚ + โ”‚ โ”‚ (OPEN/FULL/CLOSED) โ”‚ + โ”‚ โ””โ”€ PUT /admin/time-slot/:id/close โ”‚ + โ”‚ โ”‚ + โ”‚ TAB 2: ๆ‰น้‡็”Ÿๆˆ (Batch Generate) โ”‚ + โ”‚ โ”œโ”€ Start/end date pickers โ”‚ + โ”‚ โ”œโ”€ POST /admin/generate-slots โ”‚ + โ”‚ โ””โ”€ Backend: โ”‚ + โ”‚ 1. Fetch active WeekTemplates โ”‚ + โ”‚ 2. For each day in range: โ”‚ + โ”‚ - Get ISO weekday (1-7) โ”‚ + โ”‚ - Find matching templates โ”‚ + โ”‚ - Create TimeSlot records โ”‚ + โ”‚ 3. Returns { count: N } โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## Data Flow: Template โ†’ Slots + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ADMIN TEMPLATE SETUP โ”‚ +โ”‚ (weeks/admin/week-template.vue) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Admin configures templates: โ”‚ + โ”‚ โ”‚ + โ”‚ ๅ‘จไธ€: 09:00-10:00 (10 ppl) โ”‚ + โ”‚ ๅ‘จไธ€: 18:00-19:00 (8 ppl) โ”‚ + โ”‚ ๅ‘จไธ‰: 10:00-11:00 (12 ppl) โ”‚ + โ”‚ ๅ‘จไบ”: 18:00-20:00 (15 ppl) โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ PUT /admin/week-template โ”‚ + โ”‚ (All templates replaced) โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Backend: Delete all, Create new (atomic) โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Scheduler (nightly cron or manual trigger)โ”‚ + โ”‚ POST /admin/generate-slots (14 days) โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ SlotGeneratorService.generateSlots() โ”‚ + โ”‚ โ”‚ + โ”‚ For each active template: โ”‚ + โ”‚ For date in next 14 days: โ”‚ + โ”‚ If template.dayOfWeek == date.dayOfWeek: + โ”‚ CREATE TimeSlot { โ”‚ + โ”‚ date, startTime, endTime, โ”‚ + โ”‚ capacity, source=TEMPLATE, โ”‚ + โ”‚ templateId โ”‚ + โ”‚ } โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ GENERATED TIME SLOTS โ”‚ + โ”‚ โ”‚ + โ”‚ 2026-04-06 (Mon): โ”‚ + โ”‚ 09:00-10:00 (10 ppl, OPEN) โ”‚ + โ”‚ 18:00-19:00 (8 ppl, OPEN) โ”‚ + โ”‚ โ”‚ + โ”‚ 2026-04-08 (Wed): โ”‚ + โ”‚ 10:00-11:00 (12 ppl, OPEN) โ”‚ + โ”‚ โ”‚ + โ”‚ 2026-04-11 (Fri): โ”‚ + โ”‚ 18:00-20:00 (15 ppl, OPEN) โ”‚ + โ”‚ ... (more dates) โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Members can book available slots โ”‚ + โ”‚ GET /time-slot/available?date=YYYY-MM-DD + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## State Management + +### Component State (week-template.vue) +```typescript +templates: LocalTemplate[] โ—„โ”€ Main data array +loading: boolean โ—„โ”€ Fetch state +saving: boolean โ—„โ”€ Save state +isDirty: boolean โ—„โ”€ "Save bar" trigger +showModal: boolean โ—„โ”€ Modal visibility +editTarget: LocalTemplate | null โ—„โ”€ Which template is being edited +form: { โ—„โ”€ Modal form data + dayIdx: number + startTime: string + endTime: string + capacityStr: string +} +grouped: Computed> โ—„โ”€ Grouped by dayOfWeek +``` + +### Store State (stores/admin.ts) +```typescript +weekTemplates: WeekTemplate[] โ—„โ”€ Cached from server +cardTypes: CardType[] +studioConfig: StudioConfig | null +// ...other admin state +``` + +## API Endpoints Summary + +### Week Templates +``` +GET /admin/week-template Fetch all templates +PUT /admin/week-template Replace all templates +``` + +### Time Slots +``` +GET /admin/time-slots?date=YYYY-MM-DD Fetch slots for date +POST /admin/time-slot/manual Create manual slot +PUT /admin/time-slot/:id/close Close a slot +POST /admin/generate-slots Generate slots from templates +``` + +### Public Endpoints +``` +GET /time-slot/available?date=YYYY-MM-DD For members +GET /time-slot/:id For members +``` + +## Entity Relationships + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ WeekTemplate โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ id โ”‚ +โ”‚ dayOfWeek (1-7) โ”‚ +โ”‚ startTime โ”‚ +โ”‚ endTime โ”‚ +โ”‚ capacity โ”‚ +โ”‚ isActive โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ (1:N) + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ TimeSlot โ”‚ โ”‚ Booking (M:1) โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ id โ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”‚ timeSlotId โ”‚ +โ”‚ date โ”‚ โ”‚ userId โ”‚ +โ”‚ startTime โ”‚ โ”‚ status โ”‚ +โ”‚ endTime โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โ”‚ capacity โ”‚ +โ”‚ bookedCount โ”‚ +โ”‚ status โ”‚ +โ”‚ source (TEMPLATE/ โ”‚ +โ”‚ MANUAL) โ”‚ +โ”‚ templateId (FK) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## Weekday Mapping + +### Frontend Picker (dayOptions) +``` +Index 0: ๅ‘จไธ€ (Monday) โ”€โ”€โ–บ dayOfWeek = 1 +Index 1: ๅ‘จไบŒ (Tuesday) โ”€โ”€โ–บ dayOfWeek = 2 +Index 2: ๅ‘จไธ‰ (Wednesday) โ”€โ”€โ–บ dayOfWeek = 3 +Index 3: ๅ‘จๅ›› (Thursday) โ”€โ”€โ–บ dayOfWeek = 4 +Index 4: ๅ‘จไบ” (Friday) โ”€โ”€โ–บ dayOfWeek = 5 +Index 5: ๅ‘จๅ…ญ (Saturday) โ”€โ”€โ–บ dayOfWeek = 6 +Index 6: ๅ‘จๆ—ฅ (Sunday) โ”€โ”€โ–บ dayOfWeek = 7 +``` + +### Backend Conversion (slot-generator.service.ts) +```typescript +JS getDay(): 0=Sun, 1=Mon, 2=Tue, ..., 6=Sat + โ”‚ + โ–ผ toIsoWeekday() + โ”‚ +ISO weekday: 1=Mon, 2=Tue, ..., 7=Sun +``` + +## Timeline Example + +``` +TODAY: 2026-04-05 (Sunday) + +Admin actions: + 1. Sets up weekly templates for Mon-Fri + 2. Taps "ไฟๅญ˜ๅ…จ้ƒจๆ›ดๆ”น" + 3. PUT /admin/week-template sent + +Backend scheduler (daily at midnight): + 4. Runs generateSlots(14) + 5. Tomorrow is 2026-04-06 (Monday) + 6. Generates slots for Apr 6-19 (next 14 days) + 7. Creates TimeSlots based on active templates: + +Generated slots: + 2026-04-06 (Mon): 09:00-10:00, 18:00-19:00 + 2026-04-07 (Tue): (none if no templates) + 2026-04-08 (Wed): 10:00-11:00 + 2026-04-09 (Thu): (none if no templates) + 2026-04-10 (Fri): 18:00-20:00 + 2026-04-11 (Sat): (none - weekend) + 2026-04-12 (Sun): (none - weekend) + ...repeats until 2026-04-19 + +Members can book from Apr 6 onwards +``` + diff --git a/SCHEDULING_QUICK_REFERENCE.md b/SCHEDULING_QUICK_REFERENCE.md new file mode 100644 index 0000000..76e5e2a --- /dev/null +++ b/SCHEDULING_QUICK_REFERENCE.md @@ -0,0 +1,296 @@ +# Admin Scheduling - Quick Reference Guide + +## ๐ŸŽฏ Quick Links to Key Files + +### Frontend Components +| File | Lines | Purpose | +|------|-------|---------| +| `packages/app/src/pages/admin/index.vue` | 1-177 | Admin dashboard, 6 nav items | +| `packages/app/src/pages/admin/week-template.vue` | 1-500 | **MAIN: Schedule template management** | +| `packages/app/src/pages/admin/slot-adjust.vue` | 1-428 | 3 tabs: add/close/generate slots | +| `packages/app/src/stores/admin.ts` | 1-171 | API calls (Pinia store) | + +### Backend Services +| File | Purpose | +|------|---------| +| `packages/server/src/time-slot/time-slot.controller.ts` | API endpoints (/admin/*) | +| `packages/server/src/time-slot/time-slot.service.ts` | Template & slot logic | +| `packages/server/src/time-slot/slot-generator.service.ts` | Auto-generate slots from templates | +| `packages/server/src/time-slot/dto/week-template.dto.ts` | Input validation | + +### Shared Types & Constants +| File | Exports | +|------|---------| +| `packages/shared/src/types/week-template.ts` | `WeekTemplate`, `WeekTemplateInput` | +| `packages/shared/src/types/time-slot.ts` | `TimeSlot`, `CreateManualSlotDto` | +| `packages/shared/src/constants.ts` | `WEEKDAY_LABELS`, `SLOT_GENERATION_DAYS`, etc. | + +--- + +## ๐Ÿ”„ The Flow: In 30 Seconds + +``` +Admin edits templates + โ†“ +isDirty = true โ†’ Save bar appears + โ†“ +Admin taps "ไฟๅญ˜ๅ…จ้ƒจๆ›ดๆ”น" + โ†“ +PUT /admin/week-template (full array) + โ†“ +Backend: DELETE all, CREATE new (atomic) + โ†“ +Scheduler triggers (nightly or manual) + โ†“ +POST /admin/generate-slots + โ†“ +SlotGeneratorService fetches active templates + โ†“ +For each day (next 14 days): + Match templates by ISO weekday + Create TimeSlot records (source=TEMPLATE) + โ†“ +Members see slots and can book +``` + +--- + +## ๐Ÿ“Š Core Entities + +### WeekTemplate (Database) +```typescript +id: string // UUID +dayOfWeek: number // 1=Mon, 2=Tue, ..., 7=Sun +startTime: string // "09:00" +endTime: string // "10:00" +capacity: number // Max bookings +isActive: boolean // Enabled/disabled +createdAt: string +updatedAt: string +``` + +### TimeSlot (Database) +```typescript +id: string +date: string // YYYY-MM-DD +startTime: string +endTime: string +capacity: number +bookedCount: number // How many booked +status: "OPEN" | "FULL" | "CLOSED" +source: "TEMPLATE" | "MANUAL" +templateId: string | null // Links to WeekTemplate +createdAt: string +updatedAt: string +``` + +--- + +## ๐ŸŒ API Endpoints + +### GET /admin/week-template +Returns all templates (ordered by dayOfWeek ASC, startTime ASC) +```json +[ + { + "id": "uuid1", + "dayOfWeek": 1, + "startTime": "09:00", + "endTime": "10:00", + "capacity": 10, + "isActive": true, + "createdAt": "2026-04-05T00:00:00Z", + "updatedAt": "2026-04-05T00:00:00Z" + } +] +``` + +### PUT /admin/week-template +Replace all templates (atomic transaction) +```json +{ + "templates": [ + { "dayOfWeek": 1, "startTime": "09:00", "endTime": "10:00", "capacity": 10, "isActive": true }, + { "dayOfWeek": 1, "startTime": "18:00", "endTime": "19:00", "capacity": 8, "isActive": true }, + { "dayOfWeek": 3, "startTime": "10:00", "endTime": "11:00", "capacity": 12, "isActive": false } + ] +} +``` + +### POST /admin/time-slot/manual +Create a one-off slot +```json +{ + "date": "2026-04-15", + "startTime": "14:00", + "endTime": "15:00", + "capacity": 10 +} +``` + +### PUT /admin/time-slot/:id/close +Close a slot (changes status to CLOSED) + +### POST /admin/generate-slots +Generate slots for next 14 days from active templates +Response: `{ "count": 28 }` + +--- + +## ๐ŸŽจ UI State Management + +### week-template.vue Local State +```typescript +// Main data +templates: LocalTemplate[] // All templates +grouped: Computed> // By dayOfWeek + +// UI states +loading: boolean // Initial fetch +saving: boolean // Save in progress +isDirty: boolean // Show save bar? +showModal: boolean // Show add/edit modal? +editTarget: LocalTemplate | null // Editing which template? + +// Modal form +form: { + dayIdx: number // 0-6 (picker index) + startTime: string // "09:00" + endTime: string // "10:00" + capacityStr: string // User input as string +} +``` + +### Key Computed +```typescript +const grouped = computed(() => { + // Groups templates by dayOfWeek for rendering + // Sorts by day number ascending (1-7) + // Returns: { 1: [...], 3: [...], 5: [...], ... } +}) +``` + +--- + +## ๐Ÿ” Permissions & Auth + +All `/admin/*` endpoints require: +1. Valid JWT token in `Authorization: Bearer ` header +2. User role must be `UserRole.ADMIN` +3. Guards: `@UseGuards(JwtAuthGuard, RolesGuard)` + +--- + +## ๐Ÿงฎ Important Constants + +From `packages/shared/src/constants.ts`: +```typescript +SLOT_GENERATION_DAYS = 14 // Generate 14 days ahead +DEFAULT_SLOT_CAPACITY = 1 // Private lesson default +DEFAULT_CANCEL_HOURS_LIMIT = 2 // Cancel up to 2 hours before +WEEKDAY_LABELS = [ + '', // index 0 (unused) + 'ๅ‘จไธ€', // index 1 โ†’ dayOfWeek 1 (Monday) + 'ๅ‘จไบŒ', // index 2 โ†’ dayOfWeek 2 + 'ๅ‘จไธ‰', // ... etc + 'ๅ‘จๅ››', + 'ๅ‘จไบ”', + 'ๅ‘จๅ…ญ', + 'ๅ‘จๆ—ฅ' // index 7 โ†’ dayOfWeek 7 (Sunday) +] +``` + +--- + +## ๐Ÿ› Common Gotchas + +### 1. dayOfWeek vs JS getDay() +- **Frontend uses**: ISO weekday (1=Mon, 7=Sun) +- **JS Date.getDay()**: 0=Sun, 6=Sat +- **Backend converts**: `toIsoWeekday()` in slot-generator.service.ts + +### 2. Template Replace (Not Merge) +- `PUT /admin/week-template` **deletes all** and creates new +- NOT a merge/patch operation +- Frontend must send complete array + +### 3. isDirty Flag +- Tracks **any** change locally (add/edit/delete/toggle) +- Used to show/hide save bar +- Cleared after successful save + +### 4. Timezone +- All dates stored as UTC midnight: `setUTCHours(0,0,0,0)` +- Frontend displays as local YYYY-MM-DD strings +- May cause off-by-one on day boundaries + +### 5. Slot Generation +- Uses `skipDuplicates: true` in Prisma +- Safe to re-run without creating duplicates +- Assumes `date + startTime + endTime` is unique + +--- + +## ๐Ÿ’ก Usage Example: Add a Monday 9AM Class + +**Frontend (week-template.vue)**: +```typescript +// User clicks "+ ๆ–ฐๅขžๆ—ถๆฎต" +openAdd() +form.value = { dayIdx: 0, startTime: '09:00', endTime: '10:00', capacityStr: '10' } +showModal.value = true + +// User confirms in modal +submitForm() +templates.value.push({ + _key: String(Date.now()), + dayOfWeek: 1, // dayOptions[0].value = Monday + startTime: '09:00', + endTime: '10:00', + capacity: 10, + isActive: true +}) +isDirty.value = true // โ† Save bar appears + +// User taps "ไฟๅญ˜ๅ…จ้ƒจๆ›ดๆ”น" +handleSave() +payload = templates.value.map(t => ({...})) +await adminStore.saveWeekTemplates(payload) + +// Backend creates transaction: +// DELETE FROM week_template +// INSERT INTO week_template (day_of_week, start_time, end_time, capacity, is_active) +// VALUES (1, '09:00', '10:00', 10, true) +// ... (all other templates) + +// Frontend refetches and displays +``` + +--- + +## ๐Ÿ”— Related Components + +- **Admin Members** (`pages/admin/members.vue`): Shows member list +- **Admin Orders** (`pages/admin/orders.vue`): Shows order history +- **Admin Card Types** (`pages/admin/card-types.vue`): Manage membership cards +- **Admin Studio** (`pages/admin/studio.vue`): Studio info settings + +--- + +## ๐Ÿ“ˆ Scalability Notes + +### Current Approach +- Templates: Small dataset (typically < 50 records) +- Slots: Generated in batches (14 days at a time) +- Uses `skipDuplicates` to handle reruns safely + +### Bottlenecks +- Template replacement deletes ALL and creates NEW (atomic but slow with 1000s) +- Slot generation is serial (could be parallelized) +- No pagination for templates (assumes all fit in memory) + +### Future Improvements +- Batch template updates (don't replace all) +- Pagination if templates > 100 +- Incremental slot generation (detect last generated date) + diff --git a/docs/TIME_SLOT_DIAGRAMS.md b/docs/TIME_SLOT_DIAGRAMS.md new file mode 100644 index 0000000..3f19e91 --- /dev/null +++ b/docs/TIME_SLOT_DIAGRAMS.md @@ -0,0 +1,606 @@ +# Time-Slot & Scheduling System - Architecture Diagrams + +## 1. Data Model Relationships + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ WEEK TEMPLATE โ”‚ +โ”‚ โ”‚ +โ”‚ dayOfWeek (1-7, ISO standard) โ”‚ +โ”‚ startTime, endTime (e.g., "09:00", "10:00") โ”‚ +โ”‚ capacity (default 1) โ”‚ +โ”‚ isActive (can disable template) โ”‚ +โ”‚ โ”‚ +โ”‚ โ†“ (auto-generates) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ TIME SLOT โ”‚ +โ”‚ โ”‚ +โ”‚ date (calendar date, midnight UTC) โ”‚ +โ”‚ startTime, endTime (from template) โ”‚ +โ”‚ capacity (from template) โ”‚ +โ”‚ bookedCount (# of current bookings) โ”‚ +โ”‚ status (OPEN | FULL | CLOSED) โ”‚ +โ”‚ source (TEMPLATE | MANUAL) โ”‚ +โ”‚ templateId (reference to WeekTemplate) โ”‚ +โ”‚ โ”‚ +โ”‚ โ†“ (has many) โ†“ (belongs to) โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†‘ โ†‘ + โ”‚ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ”‚ + โ”‚ 1:1 booking + โ”‚ + โ†“ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ BOOKING โ”‚ +โ”‚ โ”‚ +โ”‚ userId (FK to User) โ”‚ +โ”‚ timeSlotId (FK to TimeSlot) โ”‚ +โ”‚ membershipId (FK to Membership) โ”‚ +โ”‚ status (CONFIRMED | CANCELLED | COMPLETED | NO_SHOW) โ”‚ +โ”‚ cancelledAt (timestamp when cancelled) โ”‚ +โ”‚ โ”‚ +โ”‚ Constraints: โ”‚ +โ”‚ - Unique [userId, timeSlotId] (one booking per user per slot) โ”‚ +โ”‚ - ONE booking per TimeSlot per user โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†‘ โ†‘ + โ”‚ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + belongs to + โ”‚ + โ†“ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ MEMBERSHIP โ”‚ +โ”‚ โ”‚ +โ”‚ userId (FK to User) โ”‚ +โ”‚ cardTypeId (FK to CardType) โ”‚ +โ”‚ remainingTimes (for TIMES/TRIAL card types) โ”‚ +โ”‚ expireDate (for DURATION card types) โ”‚ +โ”‚ status (ACTIVE | EXPIRED | USED_UP) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## 2. Daily Scheduler Timeline + +``` +00:00 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ”‚ + โ”œโ”€ [Midnight] - Time passes + โ”‚ + โ”œโ”€ System running in background + โ”‚ + โ”œโ”€ ... (various other operations) + โ”‚ +02:00 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ”‚ + โ”œโ”€โ–บ ๐ŸŸข SLOT GENERATION + โ”‚ SlotGeneratorService.generateSlots(14) + โ”‚ + โ”‚ โ”œโ”€ Query WeekTemplate (all isActive=true) + โ”‚ โ”œโ”€ For each day in [tomorrow, tomorrow+14): + โ”‚ โ”‚ โ”œโ”€ Get ISO weekday + โ”‚ โ”‚ โ”œโ”€ Find matching templates + โ”‚ โ”‚ โ””โ”€ Create TimeSlot entries + โ”‚ โ”œโ”€ Batch insert with skipDuplicates: true + โ”‚ โ””โ”€ Log: "Generated X new time slots" + โ”‚ +02:30 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ”‚ + โ”œโ”€โ–บ ๐ŸŸก SLOT CLEANUP + โ”‚ SlotGeneratorService.cleanupExpiredSlots() + โ”‚ + โ”‚ โ”œโ”€ Find all OPEN slots with date < TODAY + โ”‚ โ”œโ”€ Mark as CLOSED + โ”‚ โ””โ”€ Log: "Closed X expired time slots" + โ”‚ +03:00 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ”‚ + โ”œโ”€โ–บ ๐ŸŸ  MEMBERSHIP CHECK + โ”‚ SlotGeneratorService.checkExpiredMemberships() + โ”‚ + โ”‚ โ”œโ”€ Update ACTIVE memberships with expireDate < NOW + โ”‚ โ”‚ โ””โ”€ Set status = EXPIRED + โ”‚ โ”œโ”€ Update ACTIVE memberships with remainingTimes = 0 + โ”‚ โ”‚ โ””โ”€ Set status = USED_UP + โ”‚ โ””โ”€ Log: "Expired X by date, Y by sessions" + โ”‚ + โ”œโ”€ ... (users awake, making bookings) + โ”‚ +22:00 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ”‚ + โ”œโ”€โ–บ ๐Ÿ”ด BOOKING COMPLETION + โ”‚ SlotGeneratorService.completeBookings() + โ”‚ + โ”‚ โ”œโ”€ Find CONFIRMED bookings with timeSlot.date < TODAY + โ”‚ โ”œโ”€ Mark as COMPLETED + โ”‚ โ””โ”€ Log: "Completed X past bookings" + โ”‚ + โ””โ”€ (Day ends, repeat tomorrow) +``` + +--- + +## 3. Booking Lifecycle + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ BOOKING CREATION โ”‚ +โ”‚ (POST /booking) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”œโ”€ Input: { timeSlotId, membershipId } + โ”‚ + โ”œโ”€ TRANSACTION START โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ”‚ โ”‚ + โ”‚ โ”œโ”€โ–บ Fetch TimeSlot + โ”‚ โ”‚ โ””โ”€ Check: status = OPEN? โœ“ + โ”‚ โ”‚ + โ”‚ โ”œโ”€โ–บ Check Duplicate + โ”‚ โ”‚ โ””โ”€ Query: SELECT * FROM bookings WHERE userId=? AND timeSlotId=? + โ”‚ โ”‚ โ””โ”€ Must not exist + โ”‚ โ”‚ + โ”‚ โ”œโ”€โ–บ Fetch Membership + โ”‚ โ”‚ โ””โ”€ Check: belongs to user? โœ“ + โ”‚ โ”‚ โ””โ”€ Check: status = ACTIVE? โœ“ + โ”‚ โ”‚ โ””โ”€ Check: has capacity? + โ”‚ โ”‚ โ””โ”€ IF TIMES/TRIAL: remainingTimes > 0? โœ“ + โ”‚ โ”‚ โ””โ”€ IF DURATION: expireDate > NOW? โœ“ + โ”‚ โ”‚ + โ”‚ โ”œโ”€โ–บ CREATE Booking(CONFIRMED) + โ”‚ โ”‚ โ””โ”€ INSERT: { userId, timeSlotId, membershipId, status: CONFIRMED } + โ”‚ โ”‚ + โ”‚ โ”œโ”€โ–บ UPDATE TimeSlot + โ”‚ โ”‚ โ”œโ”€ bookedCount = bookedCount + 1 + โ”‚ โ”‚ โ”œโ”€ IF bookedCount >= capacity: + โ”‚ โ”‚ โ”‚ โ””โ”€ status = FULL + โ”‚ โ”‚ โ””โ”€ ELSE: + โ”‚ โ”‚ โ””โ”€ status = OPEN (unchanged) + โ”‚ โ”‚ + โ”‚ โ”œโ”€โ–บ UPDATE Membership (if TIMES/TRIAL) + โ”‚ โ”‚ โ”œโ”€ remainingTimes = remainingTimes - 1 + โ”‚ โ”‚ โ”œโ”€ IF remainingTimes <= 0: + โ”‚ โ”‚ โ”‚ โ””โ”€ status = USED_UP + โ”‚ โ”‚ โ””โ”€ ELSE: + โ”‚ โ”‚ โ””โ”€ status = ACTIVE (unchanged) + โ”‚ โ”‚ + โ”‚ โ””โ”€ TRANSACTION COMMIT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ”‚ + โ””โ”€โ–บ Return: BookingWithRelations (includes timeSlot, membership) + + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ BOOKING CANCELLATION โ”‚ +โ”‚ (PUT /booking/:id/cancel) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”œโ”€ Fetch Booking + TimeSlot + Membership + โ”‚ + โ”œโ”€ Check: booking.status = CONFIRMED? โœ“ + โ”‚ + โ”œโ”€ Calculate Refund Eligibility + โ”‚ โ”‚ + โ”‚ โ”œโ”€ cancelHoursLimit = StudioConfig.cancelHoursLimit (default 2) + โ”‚ โ”œโ”€ slotStartMs = Date(timeSlot.date) + timeSlot.startTime + โ”‚ โ”œโ”€ deadlineMs = NOW + (cancelHoursLimit * 3600 * 1000) + โ”‚ โ”‚ + โ”‚ โ”œโ”€ IF slotStartMs >= deadlineMs: + โ”‚ โ”‚ โ””โ”€ withinLimit = TRUE โœ“ (User gets refund) + โ”‚ โ””โ”€ ELSE: + โ”‚ โ””โ”€ withinLimit = FALSE (No refund) + โ”‚ + โ”œโ”€ TRANSACTION START โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ”‚ โ”‚ + โ”‚ โ”œโ”€โ–บ UPDATE Booking + โ”‚ โ”‚ โ”œโ”€ status = CANCELLED + โ”‚ โ”‚ โ””โ”€ cancelledAt = NOW + โ”‚ โ”‚ + โ”‚ โ”œโ”€โ–บ UPDATE TimeSlot + โ”‚ โ”‚ โ”œโ”€ bookedCount = MAX(0, bookedCount - 1) + โ”‚ โ”‚ โ”œโ”€ IF slot was FULL: + โ”‚ โ”‚ โ”‚ โ””โ”€ status = OPEN + โ”‚ โ”‚ โ””โ”€ ELSE: + โ”‚ โ”‚ โ””โ”€ status = (unchanged) + โ”‚ โ”‚ + โ”‚ โ”œโ”€โ–บ IF withinLimit = TRUE: + โ”‚ โ”‚ โ””โ”€ UPDATE Membership (if TIMES/TRIAL) + โ”‚ โ”‚ โ”œโ”€ remainingTimes = remainingTimes + 1 + โ”‚ โ”‚ โ”œโ”€ IF was USED_UP: + โ”‚ โ”‚ โ”‚ โ””โ”€ status = ACTIVE + โ”‚ โ”‚ โ””โ”€ ELSE: + โ”‚ โ”‚ โ””โ”€ status = (unchanged) + โ”‚ โ”‚ + โ”‚ โ””โ”€ TRANSACTION COMMIT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ”‚ + โ””โ”€โ–บ Return: { booking, refunded: boolean } +``` + +--- + +## 4. Slot Generation from Template + +``` +Template Setup: +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ PUT /admin/week-template โ”‚ +โ”‚ โ”‚ +โ”‚ { โ”‚ +โ”‚ "templates": [ โ”‚ +โ”‚ { โ”‚ +โ”‚ "dayOfWeek": 1, // Monday (ISO standard)โ”‚ +โ”‚ "startTime": "09:00", โ”‚ +โ”‚ "endTime": "10:00", โ”‚ +โ”‚ "capacity": 1, // Private lesson โ”‚ +โ”‚ "isActive": true โ”‚ +โ”‚ }, โ”‚ +โ”‚ { โ”‚ +โ”‚ "dayOfWeek": 5, // Friday (ISO standard)โ”‚ +โ”‚ "startTime": "18:00", โ”‚ +โ”‚ "endTime": "19:00", โ”‚ +โ”‚ "capacity": 1, โ”‚ +โ”‚ "isActive": true โ”‚ +โ”‚ } โ”‚ +โ”‚ ] โ”‚ +โ”‚ } โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†“ + Stored in database as WeekTemplates + โ†“ + Each day at 02:00 UTC, generateSlots(14) runs: + + + Today: Monday, April 7, 2026 + โ”‚ + โ”œโ”€ Tomorrow = Tuesday, April 8 + โ”‚ + โ”œโ”€ For next 14 days: + โ”‚ + โ”‚ Day 0: Tue (ISO 2) โ†’ no matching template โ†’ skip + โ”‚ Day 1: Wed (ISO 3) โ†’ no matching template โ†’ skip + โ”‚ Day 2: Thu (ISO 4) โ†’ no matching template โ†’ skip + โ”‚ Day 3: Fri (ISO 5) โ†’ MATCH! template (18:00-19:00) + โ”‚ โ””โ”€ CREATE TimeSlot(date=Apr12, time=18:00-19:00, capacity=1) + โ”‚ + โ”‚ Day 4: Sat (ISO 6) โ†’ no matching template โ†’ skip + โ”‚ Day 5: Sun (ISO 7) โ†’ no matching template โ†’ skip + โ”‚ Day 6: Mon (ISO 1) โ†’ MATCH! template (09:00-10:00) + โ”‚ โ””โ”€ CREATE TimeSlot(date=Apr14, time=09:00-10:00, capacity=1) + โ”‚ + โ”‚ Day 7: Tue (ISO 2) โ†’ no matching template โ†’ skip + โ”‚ ... (repeats pattern) + โ”‚ + โ””โ”€ All created with: + โ”œโ”€ status = OPEN + โ”œโ”€ bookedCount = 0 + โ”œโ”€ source = TEMPLATE + โ”œโ”€ templateId = (reference to template) + โ””โ”€ skipDuplicates = true (safe to re-run) + + +Result: + 14 Friday 18:00-19:00 slots generated + 14 Monday 09:00-10:00 slots generated + Total: 28 new slots +``` + +--- + +## 5. User Booking Flow (Frontend โ†’ Backend) + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ MEMBER CLIENT โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”‚ 1. Click "View Available Slots" + โ”‚ + โ”œโ”€โ–บ GET /time-slot/available?date=2026-04-10 + โ”‚ + โ”‚ Response: [{ + โ”‚ id: "slot-123", + โ”‚ date: "2026-04-10", + โ”‚ startTime: "09:00", + โ”‚ endTime: "10:00", + โ”‚ status: "OPEN", + โ”‚ bookedCount: 0, + โ”‚ capacity: 1, + โ”‚ isBookedByMe: false, โ† User's booking status + โ”‚ myBookingId: null + โ”‚ }, ...] + โ”‚ + โ”œโ”€ Display available slots in UI + โ”‚ + โ”‚ 2. User selects slot and membership + โ”‚ + โ”œโ”€โ–บ POST /booking + โ”‚ Body: { + โ”‚ "timeSlotId": "slot-123", + โ”‚ "membershipId": "mem-456" + โ”‚ } + โ”‚ + โ”‚ Response: { + โ”‚ id: "booking-789", + โ”‚ userId: "user-001", + โ”‚ timeSlotId: "slot-123", + โ”‚ status: "CONFIRMED", + โ”‚ createdAt: "2026-04-05T10:30:00Z", + โ”‚ timeSlot: { ... }, โ† Full slot details + โ”‚ membership: { ... } โ† Full membership details + โ”‚ } + โ”‚ + โ”œโ”€ Display confirmation + โ”‚ + โ”‚ 3. [Later] User cancels booking + โ”‚ + โ””โ”€โ–บ PUT /booking/booking-789/cancel + + Response: { + booking: { ... }, + refunded: true โ† Was refund issued? + } + + Display: "Booking cancelled. You've been refunded." +``` + +--- + +## 6. State Transitions + +### TimeSlot Status + +``` + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ AUTO-GENERATED โ”‚ + โ”‚ by generateSlots() โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ†“ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ OPEN โ”‚ โ† Can accept bookings + โ”‚ (bookedCount < โ”‚ bookedCount starts at 0 + โ”‚ capacity) โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ + [booking โ”‚ [cleanup + creates] โ”‚ or manual + โ”‚ close] + โ”‚ โ”‚ โ”‚ + โ†“ โ†“ โ†“ + FULL CLOSED + (bookedCount >= capacity) + โ”‚ + โ”‚ [booking cancelled] + โ†“ + OPEN (back to) + + + Once slot date passes: + โ”œโ”€ OPEN โ†’ CLOSED (by cleanup job at 02:30 UTC) + โ”œโ”€ FULL โ†’ CLOSED (when cleanup runs) + โ””โ”€ CANCELLED bookings don't affect slot status +``` + +### Booking Status + +``` + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ CONFIRMED โ”‚ โ† Default when created + โ”‚ โ”‚ User has active reservation + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ” + โ”‚ โ”‚ โ”‚ + [user โ”‚ [auto-mark + cancels] โ”‚ when date + โ”‚ โ”‚ passes] + โ”‚ โ”‚ โ”‚ + โ†“ โ†“ โ†“ + CANCELLED COMPLETED + (free (slot time + cancellation has passed) + until deadline) + + + CANCELLED bookings stay in history + COMPLETED bookings show in past bookings + CONFIRMED bookings show in upcoming bookings +``` + +### Membership Status + +``` + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ ACTIVE โ”‚ โ† Can book classes + โ”‚ โ”‚ Has remaining capacity + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ” + โ”‚ โ”‚ โ”‚ + [booking โ”‚ [auto-check + depletes โ”‚ by scheduler + sessions] โ”‚ at 03:00 UTC] + โ”‚ โ”‚ โ”‚ + โ†“ โ†“ โ†“ + USED_UP EXPIRED + (for (for + TIMES) DURATION) + + + USED_UP: remainingTimes = 0 (for TIMES/TRIAL only) + EXPIRED: expireDate < NOW (for DURATION) OR date-based expiry + + All non-ACTIVE statuses prevent new bookings +``` + +--- + +## 7. Timezone & Date Handling + +``` +User Timezone: Local (browser/app) +API Timezone: UTC (backend) +Database: UTC + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ User in Shanghai (UTC+8) โ”‚ +โ”‚ Local time: 2026-04-10 15:00:00 CST โ”‚ +โ”‚ UTC time: 2026-04-10 07:00:00 UTC โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”œโ”€ Query: GET /time-slot/available?date=2026-04-10 + โ”‚ (User sends local date, frontend converts to ISO) + โ”‚ + โ”œโ”€ Backend receives: + โ”‚ โ”œโ”€ Parse "2026-04-10" + โ”‚ โ”œโ”€ Build start of day: 2026-04-10T00:00:00 UTC + โ”‚ โ”œโ”€ Build end of day: 2026-04-10T23:59:59.999 UTC + โ”‚ โ”œโ”€ Query TimeSlots WHERE date BETWEEN [00:00, 23:59] + โ”‚ + โ””โ”€ Return slots for that calendar day in UTC + + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ TimeSlot Storage (Database) โ”‚ +โ”‚ โ”‚ +โ”‚ date: 2026-04-10 (DATE type, midnight UTC) โ”‚ +โ”‚ startTime: "09:00" (string, no timezone) โ”‚ +โ”‚ endTime: "10:00" (string, no timezone) โ”‚ +โ”‚ โ”‚ +โ”‚ When combined: โ”‚ +โ”‚ Slot datetime = 2026-04-10T09:00:00 UTC โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”œโ”€ For Shanghai user (UTC+8): + โ”‚ โ””โ”€ 09:00 UTC = 17:00 CST (5 PM) + โ”‚ + โ””โ”€ For New York user (UTC-4): + โ””โ”€ 09:00 UTC = 05:00 EDT (5 AM) + + +Scheduler (UTC times): +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 02:00 UTC = Generate slots โ”‚ +โ”‚ 02:30 UTC = Cleanup โ”‚ +โ”‚ 03:00 UTC = Check memberships โ”‚ +โ”‚ 22:00 UTC = Complete bookings โ”‚ +โ”‚ โ”‚ +โ”‚ When scheduler checks "is date < today?": โ”‚ +โ”‚ โ”œโ”€ Create midnight UTC boundary โ”‚ +โ”‚ โ”œโ”€ Compare slot.date < today's midnight โ”‚ +โ”‚ โ””โ”€ Mark as CLOSED/COMPLETED if older โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## 8. Error Handling Tree + +``` +POST /booking +โ”‚ +โ”œโ”€ TimeSlot not found +โ”‚ โ””โ”€ Return: NotFoundException +โ”‚ +โ”œโ”€ TimeSlot.status โ‰  OPEN +โ”‚ โ””โ”€ Return: BadRequestException("TimeSlot is not available") +โ”‚ +โ”œโ”€ Duplicate booking exists +โ”‚ โ””โ”€ Return: ConflictException("Already booked this slot") +โ”‚ +โ”œโ”€ Membership not found +โ”‚ โ””โ”€ Return: NotFoundException +โ”‚ +โ”œโ”€ Membership.userId โ‰  current user +โ”‚ โ””โ”€ Return: ForbiddenException("Not your membership") +โ”‚ +โ”œโ”€ Membership.status โ‰  ACTIVE +โ”‚ โ””โ”€ Return: BadRequestException("Membership inactive") +โ”‚ +โ”œโ”€ Card type is TIMES/TRIAL: +โ”‚ โ”‚ +โ”‚ โ””โ”€ remainingTimes โ‰ค 0 +โ”‚ โ””โ”€ Return: BadRequestException("No remaining times") +โ”‚ +โ””โ”€ Card type is DURATION: + โ”‚ + โ””โ”€ expireDate < NOW + โ””โ”€ Return: BadRequestException("Membership expired") + + +PUT /booking/:id/cancel +โ”‚ +โ”œโ”€ Booking not found +โ”‚ โ””โ”€ Return: NotFoundException +โ”‚ +โ”œโ”€ Booking.userId โ‰  current user +โ”‚ โ””โ”€ Return: ForbiddenException("Not your booking") +โ”‚ +โ”œโ”€ Booking.status โ‰  CONFIRMED +โ”‚ โ””โ”€ Return: BadRequestException("Can't cancel this status") +โ”‚ +โ””โ”€ โœ“ Cancel successful + โ”œโ”€ Check refund eligibility + โ”œโ”€ Update booking status + โ”œโ”€ Update timeSlot bookedCount + โ””โ”€ Conditionally refund membership +``` + +--- + +## 9. Integration Points + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ APP MODULE โ”‚ +โ”‚ (packages/server/src/app.module.ts) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”œโ”€ imports: [ + โ”‚ AuthModule, + โ”‚ TimeSlotModule, โ† Time-Slot logic + โ”‚ SchedulerModule, โ† Auto jobs (cron) + โ”‚ BookingModule, โ† Booking logic + โ”‚ MembershipModule, โ† Membership checks + โ”‚ StudioModule, โ† Config (cancelHoursLimit) + โ”‚ ... + โ”‚ ] + โ”‚ + โ””โ”€ Controllers route to: + โ”‚ + โ”œโ”€ TimeSlotController (public slots viewing) + โ”œโ”€ AdminTimeSlotController (templates, admin actions) + โ”œโ”€ BookingController (create, cancel bookings) + โ””โ”€ ... (other endpoints) + + +SchedulerModule dependencies: +โ”œโ”€ ScheduleModule.forRoot() โ† Enable @Cron decorators +โ””โ”€ TimeSlotModule โ† Access to SlotGeneratorService + + +BookingModule dependencies: +โ”œโ”€ MembershipModule โ† Check membership status +โ””โ”€ StudioModule โ† Read cancelHoursLimit config + + +Services call chain: +โ”œโ”€ Controller +โ”‚ โ”œโ”€ TimeSlotService +โ”‚ โ”‚ โ””โ”€ PrismaService +โ”‚ โ””โ”€ BookingService +โ”‚ โ”œโ”€ PrismaService +โ”‚ โ”œโ”€ MembershipService +โ”‚ โ””โ”€ StudioService +``` + diff --git a/docs/TIME_SLOT_INDEX.md b/docs/TIME_SLOT_INDEX.md new file mode 100644 index 0000000..9bf07c1 --- /dev/null +++ b/docs/TIME_SLOT_INDEX.md @@ -0,0 +1,364 @@ +# Time-Slot & Scheduling System - Documentation Index + +This directory contains comprehensive documentation of the NestJS backend time-slot and scheduling system for the pilates studio booking platform. + +## ๐Ÿ“š Documentation Files + +### 1. **TIME_SLOT_SCHEDULING_SYSTEM.md** (966 lines, 24KB) +**Most comprehensive reference** - Full system analysis with all details + +**Contents:** +- Executive Summary +- Data Models (WeekTemplate, TimeSlot, Booking) with Prisma schema +- SlotGeneratorService (4 key methods: generateSlots, cleanupExpiredSlots, checkExpiredMemberships, completeBookings) +- TimeSlotService (queries and management) +- TimeSlotController & AdminTimeSlotController (all endpoints) +- SchedulerService (4 daily cron jobs at 02:00, 02:30, 03:00, 22:00 UTC) +- BookingService (integration with time slots) +- Data Flow Diagrams +- DTOs & Request/Response examples +- Shared Constants & Enums +- File Structure Summary +- Key Architectural Patterns +- Example Scenarios +- Testing Guide +- Configuration & Environment +- Performance Considerations +- Security Notes +- Future Enhancement Ideas + +**When to use:** Deep dive into how the system works, understanding all components + +--- + +### 2. **TIME_SLOT_QUICK_REFERENCE.md** (355 lines, 9KB) +**Quick lookup guide** - Essential information at a glance + +**Contents:** +- File Locations (all key files in one table) +- Key Concepts (WeekTemplate, TimeSlot, Booking) +- Daily Scheduler Jobs (quick table with times and purposes) +- Important Methods (TypeScript signatures for all key methods) +- API Endpoints (member and admin endpoints with request/response) +- Status Values (all enum values explained) +- Key Logic (booking creation & cancellation flows in pseudocode) +- Weekday Mapping (ISO standard vs JavaScript) +- Database Constraints +- Configuration +- Common Errors (troubleshooting table) +- Testing +- Development Workflow +- Architecture Highlights + +**When to use:** Quick lookup while coding, API reference, debugging errors + +--- + +### 3. **TIME_SLOT_DIAGRAMS.md** (606 lines, 25KB) +**Visual references** - ASCII diagrams and flowcharts + +**Contents:** +1. Data Model Relationships (entity diagram) +2. Daily Scheduler Timeline (24-hour cron schedule visualization) +3. Booking Lifecycle (detailed creation and cancellation flows) +4. Slot Generation from Template (step-by-step with example) +5. User Booking Flow (frontend โ†’ backend interaction) +6. State Transitions (TimeSlot, Booking, Membership status flows) +7. Timezone & Date Handling (UTC, local time conversion) +8. Error Handling Tree (decision tree for POST /booking and cancellation) +9. Integration Points (module dependencies) + +**When to use:** Understanding the big picture, presenting to team, tracing flow execution + +--- + +## ๐Ÿ” Key Information at a Glance + +### Source Code Locations + +``` +Backend Time-Slot System: +โ”œโ”€โ”€ packages/server/src/time-slot/ +โ”‚ โ”œโ”€โ”€ slot-generator.service.ts (172 lines) +โ”‚ โ”œโ”€โ”€ time-slot.service.ts (142 lines) +โ”‚ โ”œโ”€โ”€ time-slot.controller.ts (93 lines) +โ”‚ โ”œโ”€โ”€ time-slot.module.ts +โ”‚ โ””โ”€โ”€ dto/ +โ”‚ โ”œโ”€โ”€ query-slots.dto.ts +โ”‚ โ”œโ”€โ”€ create-manual-slot.dto.ts +โ”‚ โ””โ”€โ”€ week-template.dto.ts +โ”‚ +โ”œโ”€โ”€ packages/server/src/scheduler/ +โ”‚ โ”œโ”€โ”€ scheduler.service.ts (55 lines) +โ”‚ โ””โ”€โ”€ scheduler.module.ts +โ”‚ +โ”œโ”€โ”€ packages/server/src/booking/ +โ”‚ โ”œโ”€โ”€ booking.service.ts (367 lines) +โ”‚ โ”œโ”€โ”€ booking.controller.ts (82 lines) +โ”‚ โ”œโ”€โ”€ booking.module.ts +โ”‚ โ””โ”€โ”€ dto/ +โ”‚ โ””โ”€โ”€ create-booking.dto.ts +โ”‚ +โ”œโ”€โ”€ packages/server/prisma/ +โ”‚ โ””โ”€โ”€ schema.prisma (Models: WeekTemplate, TimeSlot, Booking) +โ”‚ +โ””โ”€โ”€ packages/shared/src/ + โ”œโ”€โ”€ constants.ts (Slot generation, capacity defaults) + โ”œโ”€โ”€ enums.ts (TimeSlotStatus, BookingStatus, etc.) + โ””โ”€โ”€ types/ + โ””โ”€โ”€ time-slot.ts (Type definitions) +``` + +### Daily Scheduler (UTC) + +| Time | Job | Method | +|------|-----|--------| +| 02:00 | Generate 14 days of slots | `SlotGeneratorService.generateSlots(14)` | +| 02:30 | Close expired OPEN slots | `SlotGeneratorService.cleanupExpiredSlots()` | +| 03:00 | Expire memberships | `SlotGeneratorService.checkExpiredMemberships()` | +| 22:00 | Complete past bookings | `SlotGeneratorService.completeBookings()` | + +### Important Constants + +``` +DEFAULT_SLOT_CAPACITY = 1 (private lessons) +SLOT_GENERATION_DAYS = 14 (days ahead to auto-generate) +DEFAULT_CANCEL_HOURS_LIMIT = 2 (hours before slot to allow refund) +``` + +### API Endpoints + +**Member:** +``` +GET /time-slot/available?date=YYYY-MM-DD +GET /time-slot/:id +POST /booking +PUT /booking/:id/cancel +GET /booking/my +GET /booking/my/upcoming +``` + +**Admin:** +``` +GET /admin/week-template +PUT /admin/week-template +POST /admin/time-slot/manual +PUT /admin/time-slot/:id/close +POST /admin/generate-slots +GET /admin/bookings +``` + +--- + +## ๐ŸŽฏ Common Tasks & Where to Find Info + +| Task | Reference | +|------|-----------| +| **Understand slot generation algorithm** | TIME_SLOT_SCHEDULING_SYSTEM.md ยง 2.2 or DIAGRAMS ยง 4 | +| **See all API endpoints** | QUICK_REFERENCE ยง "API Endpoints" or TIME_SLOT_SCHEDULING_SYSTEM.md ยง 4 | +| **Booking creation logic** | TIME_SLOT_DIAGRAMS.md ยง 3 or QUICK_REFERENCE ยง "Key Logic" | +| **Weekday mapping (ISO vs JS)** | QUICK_REFERENCE ยง "Weekday Mapping" or DIAGRAMS ยง 7 | +| **Cancellation refund policy** | TIME_SLOT_SCHEDULING_SYSTEM.md ยง 6.1 or DIAGRAMS ยง 3 | +| **Scheduler jobs timeline** | QUICK_REFERENCE ยง "Daily Scheduler Jobs" or DIAGRAMS ยง 2 | +| **Error handling** | QUICK_REFERENCE ยง "Common Errors" or DIAGRAMS ยง 8 | +| **Data model relationships** | DIAGRAMS ยง 1 or TIME_SLOT_SCHEDULING_SYSTEM.md ยง 1 | +| **Configuration & setup** | QUICK_REFERENCE ยง "Configuration" or TIME_SLOT_SCHEDULING_SYSTEM.md ยง 14 | +| **Performance tips** | TIME_SLOT_SCHEDULING_SYSTEM.md ยง 15 or QUICK_REFERENCE ยง "Performance Tips" | +| **Module dependencies** | DIAGRAMS ยง 9 or TIME_SLOT_SCHEDULING_SYSTEM.md ยง 11.2 | +| **Testing** | TIME_SLOT_SCHEDULING_SYSTEM.md ยง 13 or QUICK_REFERENCE ยง "Testing" | + +--- + +## ๐Ÿ“‹ System Overview + +### What It Does + +This system manages the complete lifecycle of time slots and bookings for a pilates studio: + +1. **Automated Slot Generation**: Every day at 02:00 UTC, generates 14 days of time slots from reusable weekly templates +2. **Capacity Management**: Tracks slot capacity and prevents overbooking +3. **Booking Management**: Allows members to book slots with their memberships +4. **Cancellation & Refunds**: Members can cancel with conditional refunds (within 2-hour window) +5. **Membership Expiration**: Automatically expires memberships by date or used sessions +6. **Cleanup**: Marks past slots as closed and completed bookings as finished + +### Key Concepts + +- **WeekTemplate**: Defines recurring schedule (e.g., "Monday 09:00-10:00") +- **TimeSlot**: Individual class instance (e.g., "April 10, 2026 09:00-10:00") +- **Booking**: User's reservation (links user + slot + membership) +- **Status Tracking**: OPEN โ†’ FULL โ†’ CLOSED (slots) and CONFIRMED โ†’ COMPLETED (bookings) + +### Architecture Highlights + +โœ… **Idempotent** - Safe to re-run slot generation +โœ… **Transactional** - ACID compliance for bookings +โœ… **Automated** - 4 daily cron jobs maintain state +โœ… **Flexible** - Supports TIMES, DURATION, and TRIAL memberships +โœ… **Scalable** - Batch operations, proper database indexes +โœ… **Secure** - Role-based access, comprehensive validation + +--- + +## ๐Ÿš€ Getting Started + +### For New Developers + +1. **Start with**: TIME_SLOT_QUICK_REFERENCE.md + - Get oriented with file locations and key methods + +2. **Then read**: TIME_SLOT_DIAGRAMS.md ยง 1 (Data Model) + - Understand how entities relate + +3. **Deep dive**: TIME_SLOT_SCHEDULING_SYSTEM.md ยง 2 + - Study the SlotGeneratorService algorithm + +4. **Explore the code**: Read actual source files for implementation details + +### For System Integration + +1. Review TIME_SLOT_DIAGRAMS.md ยง 9 (Integration Points) +2. Check the module imports in `app.module.ts` +3. Understand dependencies in QUICK_REFERENCE.md ยง "Configuration" + +### For API Integration + +1. Start with TIME_SLOT_QUICK_REFERENCE.md ยง "API Endpoints" +2. See examples in TIME_SLOT_SCHEDULING_SYSTEM.md ยง 12 +3. Check DTOs in TIME_SLOT_SCHEDULING_SYSTEM.md ยง 8 + +### For Debugging + +1. Check common errors in QUICK_REFERENCE.md ยง "Common Errors" +2. Trace error handling in DIAGRAMS.md ยง 8 +3. Review actual error handling in source code + +--- + +## ๐Ÿ“– Reading Recommendations by Role + +### Backend Developer +1. TIME_SLOT_SCHEDULING_SYSTEM.md (all) +2. TIME_SLOT_DIAGRAMS.md (all) +3. Source code in `packages/server/src/time-slot/` + +### Frontend Developer +1. TIME_SLOT_QUICK_REFERENCE.md (API Endpoints section) +2. TIME_SLOT_SCHEDULING_SYSTEM.md ยง 12 (Example Scenarios) +3. TIME_SLOT_DIAGRAMS.md ยง 5 (User Booking Flow) + +### DevOps / Sysadmin +1. TIME_SLOT_SCHEDULING_SYSTEM.md ยง 14 (Configuration) +2. TIME_SLOT_QUICK_REFERENCE.md ยง "Daily Scheduler Jobs" +3. TIME_SLOT_DIAGRAMS.md ยง 2 (Scheduler Timeline) + +### Product Manager +1. TIME_SLOT_SCHEDULING_SYSTEM.md ยง "Executive Summary" +2. TIME_SLOT_DIAGRAMS.md ยง 3 & 5 (Booking flows) +3. TIME_SLOT_QUICK_REFERENCE.md ยง "Architecture Highlights" + +### QA / Tester +1. TIME_SLOT_QUICK_REFERENCE.md (all) +2. TIME_SLOT_SCHEDULING_SYSTEM.md ยง 13 (Testing Guide) +3. TIME_SLOT_SCHEDULING_SYSTEM.md ยง 12 (Example Scenarios) + +--- + +## ๐Ÿ”— Related Documentation + +- **Database Schema**: See `packages/server/prisma/schema.prisma` (lines 113-168) +- **Shared Types**: See `packages/shared/src/types/` and `enums.ts` +- **Authentication**: See booking endpoints require JwtAuthGuard +- **Membership System**: See `BookingService` integration with `MembershipService` +- **Studio Config**: See `StudioService` for `cancelHoursLimit` + +--- + +## ๐Ÿ“Š Document Statistics + +| File | Lines | Size | Topics | +|------|-------|------|--------| +| TIME_SLOT_SCHEDULING_SYSTEM.md | 966 | 24KB | 17 comprehensive sections | +| TIME_SLOT_QUICK_REFERENCE.md | 355 | 9KB | 15 quick-lookup sections | +| TIME_SLOT_DIAGRAMS.md | 606 | 25KB | 9 visual flowcharts | +| **Total** | **1,927** | **58KB** | **Complete system coverage** | + +--- + +## ๐ŸŽ“ Learning Path + +``` +Entry Level +โ”œโ”€ README.md (this file) +โ”œโ”€ TIME_SLOT_QUICK_REFERENCE.md (20 min read) +โ””โ”€ TIME_SLOT_DIAGRAMS.md ยง 1 (5 min) + +Intermediate +โ”œโ”€ TIME_SLOT_DIAGRAMS.md (all, 15 min) +โ”œโ”€ TIME_SLOT_QUICK_REFERENCE.md (re-read, 15 min) +โ””โ”€ TIME_SLOT_SCHEDULING_SYSTEM.md ยง 1-6 (30 min) + +Advanced +โ”œโ”€ TIME_SLOT_SCHEDULING_SYSTEM.md (full, 60 min) +โ”œโ”€ Source code reading (packages/server/src/time-slot/) +โ””โ”€ Prisma schema study + +Expert +โ””โ”€ Code review + contributions +``` + +--- + +## ๐Ÿค Contributing + +When adding features or making changes: + +1. **Update the code** in `packages/server/src/time-slot/` and related modules +2. **Update tests** in `__tests__/` directories +3. **Update documentation** in this docs folder if behavior changes +4. Use the **Quick Reference** as checklist for all affected pieces + +--- + +## โ“ FAQ + +**Q: Where do time slots come from?** +A: Auto-generated from WeekTemplates every day at 02:00 UTC by `generateSlots(14)`. + +**Q: Can I disable slot generation?** +A: Yes, make templates `isActive: false` or disable the cron job in `scheduler.service.ts`. + +**Q: How is capacity managed?** +A: `bookedCount` increments on booking, slot status becomes FULL when `bookedCount >= capacity`. + +**Q: What if I cancel a booking?** +A: `bookedCount` decrements; if within 2-hour window, membership refunded; slot status restored if was FULL. + +**Q: Timezone support?** +A: All times stored in UTC. Scheduler uses UTC times (02:00, 02:30, etc.). See DIAGRAMS ยง 7. + +**Q: How are memberships expired?** +A: Automatically by scheduler job at 03:00 UTC daily; marks EXPIRED if date passed or USED_UP if sessions depleted. + +--- + +## ๐Ÿ“ž Quick Reference Card + +### Status Values +- **TimeSlot**: OPEN | FULL | CLOSED +- **Booking**: CONFIRMED | CANCELLED | COMPLETED | NO_SHOW +- **Membership**: ACTIVE | EXPIRED | USED_UP + +### Key Dates & Times +- **Slot generation**: Daily 02:00 UTC (14 days ahead) +- **Cleanup**: Daily 02:30 UTC +- **Membership check**: Daily 03:00 UTC +- **Booking completion**: Daily 22:00 UTC +- **Cancellation window**: 2 hours before slot (configurable) + +### Key Files +- **Slot generation**: `slot-generator.service.ts` +- **Slot queries**: `time-slot.service.ts` +- **Booking logic**: `booking.service.ts` +- **Database**: `prisma/schema.prisma` + diff --git a/docs/TIME_SLOT_QUICK_REFERENCE.md b/docs/TIME_SLOT_QUICK_REFERENCE.md new file mode 100644 index 0000000..0394ba7 --- /dev/null +++ b/docs/TIME_SLOT_QUICK_REFERENCE.md @@ -0,0 +1,355 @@ +# Time-Slot & Scheduling System - Quick Reference + +## File Locations + +| Component | Path | +|-----------|------| +| **Slot Generator** | `packages/server/src/time-slot/slot-generator.service.ts` | +| **TimeSlot Service** | `packages/server/src/time-slot/time-slot.service.ts` | +| **TimeSlot Controller** | `packages/server/src/time-slot/time-slot.controller.ts` | +| **Scheduler** | `packages/server/src/scheduler/scheduler.service.ts` | +| **Booking Service** | `packages/server/src/booking/booking.service.ts` | +| **Booking Controller** | `packages/server/src/booking/booking.controller.ts` | +| **Database Schema** | `packages/server/prisma/schema.prisma` | +| **Shared Constants** | `packages/shared/src/constants.ts` | +| **Shared Enums** | `packages/shared/src/enums.ts` | + +--- + +## Key Concepts + +### WeekTemplate +Defines **recurring class schedule** by day of week (1=Monday, 7=Sunday) and time. +- Used to auto-generate TimeSlots nightly +- Can be enabled/disabled +- Has capacity (default 1 for private lessons) + +### TimeSlot +**Individual class instance** on a specific date with a specific time. +- Status: OPEN โ†’ FULL โ†’ CLOSED +- Source: TEMPLATE (auto-generated) or MANUAL (admin-created) +- Cannot have duplicates (unique constraint on date+startTime+endTime) + +### Booking +**User's reservation** for a specific TimeSlot. +- Status: CONFIRMED โ†’ COMPLETED (or CANCELLED) +- Links user + timeSlot + membership +- Unique constraint: one booking per user per slot + +--- + +## Daily Scheduler Jobs + +All times in UTC: + +| Time | Job | What It Does | +|------|-----|--------------| +| **02:00** | `handleSlotGeneration()` | Generate slots 14 days ahead from WeekTemplates | +| **02:30** | `handleCleanupSlots()` | Mark past OPEN slots as CLOSED | +| **03:00** | `handleCheckMemberships()` | Expire memberships by date or used-up sessions | +| **22:00** | `handleCompleteBookings()` | Mark past CONFIRMED bookings as COMPLETED | + +--- + +## Important Methods + +### SlotGeneratorService + +```typescript +// Generate N days of slots from WeekTemplates +generateSlots(daysAhead = 14): Promise + +// Close all past OPEN slots +cleanupExpiredSlots(): Promise + +// Expire memberships by date or session count +checkExpiredMemberships(): Promise + +// Mark past bookings as COMPLETED +completeBookings(): Promise +``` + +### TimeSlotService + +```typescript +// Get all slots for a date (with user's booking status if provided) +getAvailableSlots(date: string, userId?: string): Promise + +// Manually create a one-off slot +createManualSlot(dto: CreateManualSlotDto): Promise + +// Close a slot (prevent new bookings) +closeSlot(id: string): Promise + +// Get/replace weekly templates +getWeekTemplates(): Promise +replaceWeekTemplates(items: WeekTemplateItemDto[]): Promise +``` + +### BookingService + +```typescript +// Create a booking (validates slot/membership, updates counts) +createBooking(userId: string, dto: CreateBookingDto): Promise + +// Cancel a booking (conditionally refunds membership) +cancelBooking(userId: string, bookingId: string): Promise + +// Get user's bookings (paginated, filterable by status) +getMyBookings(userId: string, status?, page, limit): Promise + +// Get all CONFIRMED bookings for dates >= today +getUpcomingBookings(userId: string): Promise +``` + +--- + +## API Endpoints + +### Member Endpoints + +``` +GET /time-slot/available?date=2026-04-10 + โ†’ Returns slots for that date with user's booking status + +GET /time-slot/:id + โ†’ Returns full slot details with all bookings + +POST /booking + Body: { "timeSlotId": "uuid", "membershipId": "uuid" } + โ†’ Create a booking + +PUT /booking/:id/cancel + โ†’ Cancel a booking (refund if within window) + +GET /booking/my?status=CONFIRMED&page=1&limit=10 + โ†’ Get user's bookings (paginated) + +GET /booking/my/upcoming + โ†’ Get all upcoming CONFIRMED bookings +``` + +### Admin Endpoints + +``` +GET /admin/week-template + โ†’ List all templates + +PUT /admin/week-template + Body: { "templates": [ {...}, {...} ] } + โ†’ Replace all templates (atomic) + +POST /admin/time-slot/manual + Body: { "date", "startTime", "endTime", "capacity" } + โ†’ Create a one-off slot + +PUT /admin/time-slot/:id/close + โ†’ Close a slot + +POST /admin/generate-slots + โ†’ Manually trigger slot generation + +GET /admin/bookings?page=1&limit=10&status=CONFIRMED + โ†’ View all bookings (admin) +``` + +--- + +## Status Values + +### TimeSlotStatus +- **OPEN**: Accepts bookings (bookedCount < capacity) +- **FULL**: At capacity (bookedCount >= capacity) +- **CLOSED**: Past date or manually closed + +### BookingStatus +- **CONFIRMED**: Active reservation +- **CANCELLED**: User cancelled +- **COMPLETED**: Slot time has passed +- **NO_SHOW**: Marked manually + +### MembershipStatus +- **ACTIVE**: Valid for booking +- **EXPIRED**: End date passed +- **USED_UP**: No remaining sessions (for TIMES/TRIAL) + +### CardTypeCategory +- **TIMES**: N sessions (e.g., "5-pack") +- **DURATION**: Valid for X days (e.g., "1-month") +- **TRIAL**: Free trial sessions + +--- + +## Key Logic + +### Booking Creation Transaction + +``` +1. Validate TimeSlot exists and status = OPEN +2. Check user not already booked this slot +3. Validate Membership: + - Belongs to user + - Status = ACTIVE + - Has capacity: + * TIMES/TRIAL: remainingTimes > 0 + * DURATION: expireDate > NOW +4. CREATE Booking(CONFIRMED) +5. UPDATE TimeSlot: + - bookedCount++ + - IF bookedCount >= capacity THEN status = FULL +6. UPDATE Membership (if time-based): + - remainingTimes-- + - IF remainingTimes = 0 THEN status = USED_UP +7. Return with relations +``` + +### Cancellation Refund Logic + +``` +cancelHoursLimit = 2 (configurable in StudioConfig) +slotStartTime = TimeSlot.date + TimeSlot.startTime +deadline = NOW + (cancelHoursLimit * hours) + +IF slotStartTime >= deadline: + Refund = TRUE + Increment membership.remainingTimes +ELSE: + Refund = FALSE + No membership change +``` + +--- + +## Weekday Mapping + +**ISO Standard** (what WeekTemplate uses): +``` +1 = Monday +2 = Tuesday +3 = Wednesday +4 = Thursday +5 = Friday +6 = Saturday +7 = Sunday +``` + +**JavaScript getDay()** (what Date does): +``` +0 = Sunday +1 = Monday +2 = Tuesday +... +6 = Saturday +``` + +**Conversion function:** +```typescript +function toIsoWeekday(jsDay: number): number { + return jsDay === 0 ? 7 : jsDay +} +``` + +--- + +## Database Constraints + +### TimeSlot +- Unique: `[date, startTime, endTime]` - prevents duplicate slots +- Index: `date` - for date range queries +- Index: `status` - for filtering + +### Booking +- Unique: `[userId, timeSlotId]` - one booking per user per slot +- Index: `userId` - for user's bookings +- Index: `status` - for status filtering + +--- + +## Configuration + +### Environment Variables +``` +DATABASE_URL=mysql://... (required) +``` + +### From StudioConfig Table +``` +cancelHoursLimit = 2 (hours before slot to allow free cancellation) +``` + +### From Shared Constants +``` +DEFAULT_SLOT_CAPACITY = 1 +SLOT_GENERATION_DAYS = 14 +DEFAULT_CANCEL_HOURS_LIMIT = 2 +``` + +--- + +## Common Errors + +| Error | Cause | Solution | +|-------|-------|----------| +| TimeSlot not found | Invalid slot ID | Check slot exists | +| TimeSlot is not available | Status โ‰  OPEN | Slot is FULL or CLOSED | +| You have already booked this slot | Duplicate booking | Check user's bookings | +| This membership does not belong to you | Membership not user's | Verify membership | +| Membership is not active | Status โ‰  ACTIVE | Renew or purchase membership | +| No remaining times on this membership | remainingTimes โ‰ค 0 | Purchase more sessions | +| Membership has expired | expireDate < NOW | Renew membership | +| Cannot cancel booking with status | Status โ‰  CONFIRMED | Can only cancel CONFIRMED bookings | + +--- + +## Testing + +Run tests with: +```bash +npm test -- slot-generator.service.spec.ts +npm test -- booking.service.spec.ts +npm test -- time-slot.service.spec.ts +``` + +Key test areas: +- Slot generation from templates +- Weekday mapping (JS vs ISO) +- Booking creation with all validations +- Cancellation with/without refund +- Membership expiration + +--- + +## Performance Tips + +1. **Avoid N+1 queries** - Always include relations in findMany +2. **Batch operations** - Use createMany/updateMany for large operations +3. **Transactions** - Wrap multi-step operations to prevent race conditions +4. **Indexes** - Queries filter by date and status (both indexed) + +--- + +## Development Workflow + +1. **Setup templates** โ†’ `PUT /admin/week-template` +2. **Manually trigger generation** โ†’ `POST /admin/generate-slots` +3. **View available slots** โ†’ `GET /time-slot/available?date=...` +4. **Create booking** โ†’ `POST /booking` +5. **Cancel booking** โ†’ `PUT /booking/:id/cancel` + +For testing without scheduler: +```typescript +// Inject SlotGeneratorService and call directly +const count = await slotGenerator.generateSlots(7) +``` + +--- + +## Architecture Highlights + +โœ… **Idempotent** - Safe to re-run slot generation +โœ… **Transactional** - Bookings are atomic +โœ… **Automated** - 4 daily cron jobs maintain state +โœ… **Flexible** - Supports multiple membership types +โœ… **Scalable** - Batch operations, proper indexes +โœ… **Validating** - DTO decorators + business logic checks + diff --git a/docs/TIME_SLOT_SCHEDULING_SYSTEM.md b/docs/TIME_SLOT_SCHEDULING_SYSTEM.md new file mode 100644 index 0000000..fc63899 --- /dev/null +++ b/docs/TIME_SLOT_SCHEDULING_SYSTEM.md @@ -0,0 +1,966 @@ +# NestJS Time-Slot & Scheduling System Analysis + +## Executive Summary + +This is a comprehensive analysis of the pilates studio booking system's time-slot generation and scheduling backend. The system automatically generates time slots from reusable weekly templates, maintains their lifecycle, and integrates tightly with the booking and membership management systems. + +--- + +## 1. Data Models (Prisma Schema) + +### 1.1 WeekTemplate Model +**Location:** `packages/server/prisma/schema.prisma` (lines 113-126) + +```prisma +model WeekTemplate { + id String @id @default(uuid()) + dayOfWeek Int @map("day_of_week") // 1=Mon, 7=Sun (ISO standard) + startTime String @map("start_time") // e.g., "09:00" + endTime String @map("end_time") // e.g., "10:00" + capacity Int @default(1) // Max participants + isActive Boolean @default(true) // Enable/disable template + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + timeSlots TimeSlot[] // Generated slots from this template +} +``` + +**Purpose:** +- Defines recurring time slots by day of week and time +- Used as blueprint for automatic slot generation +- Capacity defines how many people can book each slot + +**Key Constraints:** +- `dayOfWeek` uses **ISO 8601 standard** (1=Monday through 7=Sunday) +- NOT JavaScript getDay() (0=Sunday) +- Conversion happens in SlotGeneratorService.toIsoWeekday() + +--- + +### 1.2 TimeSlot Model +**Location:** `packages/server/prisma/schema.prisma` (lines 128-148) + +```prisma +model TimeSlot { + id String @id @default(uuid()) + date DateTime @db.Date // Calendar date (midnight UTC) + startTime String @map("start_time") // "HH:mm" format + endTime String @map("end_time") // "HH:mm" format + capacity Int @default(1) // Max participants + bookedCount Int @default(0) // Current bookings + status TimeSlotStatus @default(OPEN) // OPEN | FULL | CLOSED + source TimeSlotSource @default(TEMPLATE) // TEMPLATE | MANUAL + templateId String? @map("template_id") // Reference to WeekTemplate + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + template WeekTemplate? @relation(fields: [templateId], references: [id]) + bookings Booking[] + + @@unique([date, startTime, endTime]) // Prevent duplicate slots + @@index([date]) + @@index([status]) +} +``` + +**Status Lifecycle:** +- **OPEN**: Accepts bookings, bookedCount < capacity +- **FULL**: No more bookings, bookedCount >= capacity +- **CLOSED**: Past date or manually closed, no bookings allowed + +**Source Types:** +- **TEMPLATE**: Auto-generated from WeekTemplate +- **MANUAL**: Created directly by admin + +--- + +### 1.3 Booking Model +**Location:** `packages/server/prisma/schema.prisma` (lines 150-168) + +```prisma +model Booking { + id String @id @default(uuid()) + userId String @map("user_id") + timeSlotId String @map("time_slot_id") + membershipId String @map("membership_id") + status BookingStatus @default(CONFIRMED) + cancelledAt DateTime? @map("cancelled_at") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id]) + timeSlot TimeSlot @relation(fields: [timeSlotId], references: [id]) + membership Membership @relation(fields: [membershipId], references: [id]) + + @@unique([userId, timeSlotId]) // One booking per user per slot + @@index([userId]) + @@index([status]) +} +``` + +**Booking Status Values:** +- **CONFIRMED**: Active reservation +- **CANCELLED**: User cancelled +- **COMPLETED**: Slot time has passed +- **NO_SHOW**: Marked manually if user didn't attend + +--- + +## 2. SlotGeneratorService + +**Location:** `packages/server/src/time-slot/slot-generator.service.ts` + +### 2.1 Service Overview + +Core service responsible for: +1. **Generating** time slots from WeekTemplate +2. **Cleaning up** expired slots +3. **Managing** membership expiration +4. **Marking** past bookings as completed + +### 2.2 Key Methods + +#### `generateSlots(daysAhead: number = 14): Promise` + +**Purpose:** Creates time slots for the next N days based on active WeekTemplates. + +**Algorithm:** +``` +1. Fetch all active WeekTemplates (isActive = true) +2. Calculate tomorrow at midnight UTC as start date +3. For each day in [tomorrow, tomorrow + daysAhead): + a. Get ISO weekday (1-7) from JavaScript date + b. Find matching templates for this weekday + c. For each matching template, create slot data: + - date: UTC midnight + - startTime/endTime: from template + - capacity: from template + - source: TimeSlotSource.TEMPLATE + - templateId: template.id +4. Batch create all slots using createMany() with skipDuplicates: true +5. Return count of newly created slots +``` + +**Key Features:** +- **Idempotent:** Re-running is safe; duplicate date+startTime+endTime combos are skipped +- **Timezone Aware:** Uses UTC midnight for dates +- **Weekday Mapping:** Converts JS getDay() โ†’ ISO weekday +- **Batch Insert:** Creates all slots in single database operation + +**Example Execution:** +- Today: Monday, April 7, 2026 +- Daylight: 14 days +- Template: Monday 09:00-10:00, Friday 18:00-19:00 +- Result: 2 slots tomorrow (Monday), 0 Wed-Thu, 1 Friday, repeat pattern + +--- + +#### `cleanupExpiredSlots(): Promise` + +**Purpose:** Marks all OPEN slots with dates before today as CLOSED. + +**Logic:** +```sql +UPDATE time_slots +SET status = 'CLOSED' +WHERE status = 'OPEN' AND date < TODAY_MIDNIGHT_UTC +``` + +**Returns:** Count of slots closed. + +--- + +#### `checkExpiredMemberships(): Promise` + +**Purpose:** Manages membership expiration in two ways: + +1. **By Expiration Date:** + ``` + WHERE status = ACTIVE AND expireDate < NOW + SET status = EXPIRED + ``` + +2. **By Used-Up Sessions:** + ``` + WHERE status = ACTIVE AND remainingTimes = 0 + SET status = USED_UP + ``` + +**Returns:** Total count of memberships updated. + +--- + +#### `completeBookings(): Promise` + +**Purpose:** Marks CONFIRMED bookings for past time slots as COMPLETED. + +**Logic:** +```sql +UPDATE bookings +SET status = 'COMPLETED' +WHERE status = 'CONFIRMED' + AND timeSlot.date < TODAY_MIDNIGHT_UTC +``` + +--- + +## 3. TimeSlotService + +**Location:** `packages/server/src/time-slot/time-slot.service.ts` + +### 3.1 Service Overview + +Handles time slot queries and management for both members and admins. + +### 3.2 Key Methods + +#### `getAvailableSlots(date: string, userId?: string): Promise` + +**Purpose:** Retrieve all non-closed slots for a specific date, optionally including user's booking status. + +**Query Logic:** +``` +1. Parse date string to Date object +2. Find all slots for that calendar day: + - WHERE status != CLOSED + - ORDER BY startTime ASC +3. If userId provided: + - Include bookings where userId=X AND status=CONFIRMED + - Map to "isBookedByMe" and "myBookingId" fields +4. Return TimeSlotWithBookingStatus[] +``` + +**Response Type:** +```typescript +interface TimeSlotWithBookingStatus { + id: string + date: string // ISO date "YYYY-MM-DD" + startTime: string // "HH:mm" + endTime: string + capacity: number + bookedCount: number + status: TimeSlotStatus // OPEN | FULL | CLOSED + source: TimeSlotSource // TEMPLATE | MANUAL + templateId: string | null + createdAt: string // ISO datetime + updatedAt: string + isBookedByMe: boolean // Current user's booking? + myBookingId: string | null // For cancellation +} +``` + +--- + +#### `getSlotById(id: string): Promise` + +Returns full slot details including all bookings. Throws NotFoundException if not found. + +--- + +#### `createManualSlot(dto: CreateManualSlotDto): Promise` + +**Purpose:** Allow admins to create one-off time slots outside templates. + +**DTO:** +```typescript +class CreateManualSlotDto { + date: string // "YYYY-MM-DD" + startTime: string // "HH:mm" + endTime: string // "HH:mm" + capacity?: number // Defaults to DEFAULT_SLOT_CAPACITY (1) +} +``` + +**Creates slot with:** +- `source: TimeSlotSource.MANUAL` +- `templateId: null` + +--- + +#### `closeSlot(id: string): Promise` + +Sets slot status to CLOSED. Prevents new bookings but keeps existing ones. + +--- + +#### `getWeekTemplates(): Promise` + +Lists all templates ordered by dayOfWeek and startTime. + +--- + +#### `replaceWeekTemplates(items: WeekTemplateItemDto[]): Promise` + +**Purpose:** Atomic replacement of all templates (used during admin config). + +**Transaction:** +``` +1. DELETE FROM week_templates (all rows) +2. CREATE week_templates with new items +3. Return count +``` + +--- + +## 4. TimeSlotController & AdminTimeSlotController + +**Location:** `packages/server/src/time-slot/time-slot.controller.ts` + +### 4.1 Member Endpoints + +#### `GET /time-slot/available?date=YYYY-MM-DD` +- Returns available slots for the date +- Includes current user's booking status +- Requires JWT authentication + +#### `GET /time-slot/:id` +- Returns full slot details with all bookings +- Requires JWT authentication + +--- + +### 4.2 Admin Endpoints + +All require `@Roles(UserRole.ADMIN)` and JWT auth. + +#### `GET /admin/week-template` +Lists all WeekTemplate entries. + +#### `PUT /admin/week-template` +Replaces all templates. Request body: +```json +{ + "templates": [ + { + "dayOfWeek": 1, + "startTime": "09:00", + "endTime": "10:00", + "capacity": 1, + "isActive": true + } + ] +} +``` + +#### `POST /admin/time-slot/manual` +Creates a manual slot. Request body: +```json +{ + "date": "2026-04-10", + "startTime": "14:00", + "endTime": "15:00", + "capacity": 2 +} +``` + +#### `PUT /admin/time-slot/:id/close` +Closes a specific slot. + +#### `POST /admin/generate-slots` +Manually trigger slot generation (default 14 days ahead). + +--- + +## 5. SchedulerService - Automated Jobs + +**Location:** `packages/server/src/scheduler/scheduler.service.ts` + +### 5.1 Overview + +Uses `@nestjs/schedule` to run daily maintenance tasks. All times in UTC. + +### 5.2 Cron Jobs + +#### Job 1: Slot Generation +``` +@Cron('0 2 * * *') // 02:00 UTC daily +async handleSlotGeneration() +``` +- Calls: `slotGenerator.generateSlots(14)` +- Generates slots 14 days ahead +- Purpose: Keep pipeline filled + +--- + +#### Job 2: Slot Cleanup +``` +@Cron('30 2 * * *') // 02:30 UTC daily +async handleCleanupSlots() +``` +- Calls: `slotGenerator.cleanupExpiredSlots()` +- Marks past OPEN slots as CLOSED + +--- + +#### Job 3: Membership Check +``` +@Cron('0 3 * * *') // 03:00 UTC daily +async handleCheckMemberships() +``` +- Calls: `slotGenerator.checkExpiredMemberships()` +- Expires memberships by date or used-up sessions + +--- + +#### Job 4: Booking Completion +``` +@Cron('0 22 * * *') // 22:00 UTC daily +async handleCompleteBookings() +``` +- Calls: `slotGenerator.completeBookings()` +- Marks past CONFIRMED bookings as COMPLETED + +--- + +## 6. BookingService - Integration with TimeSlots + +**Location:** `packages/server/src/booking/booking.service.ts` + +### 6.1 Key Integration Points + +#### `createBooking(userId: string, dto: CreateBookingDto): Promise` + +**DTO:** +```typescript +class CreateBookingDto { + timeSlotId: string // UUID of TimeSlot + membershipId: string // UUID of Membership +} +``` + +**Transaction Flow:** +``` +1. Fetch TimeSlot - validate status = OPEN +2. Check unique constraint - user not already booked this slot +3. Fetch Membership - validate: + - Belongs to user + - Status = ACTIVE + - Has remaining capacity: + * TIMES/TRIAL: remainingTimes > 0 + * DURATION: not expired +4. Create Booking(userId, timeSlotId, membershipId) โ†’ CONFIRMED +5. Update TimeSlot: + - bookedCount++ + - If bookedCount >= capacity, set status = FULL +6. Update Membership (if time-based): + - remainingTimes-- + - If remainingTimes = 0, set status = USED_UP +7. Return booking with relations +``` + +**Error Handling:** +- TimeSlot not OPEN โ†’ BadRequestException +- Duplicate booking โ†’ ConflictException +- Invalid membership โ†’ ForbiddenException +- No remaining sessions โ†’ BadRequestException + +--- + +#### `cancelBooking(userId: string, bookingId: string): Promise` + +**Refund Logic:** +``` +cancelHoursLimit = StudioConfig.cancelHoursLimit (default 2 hours) +slotStartMs = Date(date).setUTC Hours + startTime +deadlineMs = NOW + (cancelHoursLimit * 3600 * 1000) +withinLimit = slotStartMs >= deadlineMs + +IF withinLimit: + Restore membership.remainingTimes++ +ELSE: + No refund +``` + +**Transaction Flow:** +``` +1. Mark Booking โ†’ CANCELLED, set cancelledAt +2. Decrement TimeSlot.bookedCount +3. If slot was FULL, restore to OPEN +4. If within cancel window: + - For TIMES/TRIAL: increment remainingTimes + - Restore membership status if was USED_UP +``` + +--- + +#### `getMyBookings(userId: string, status?, page, limit): Promise` + +Lists user's bookings with pagination, optionally filtered by status. + +--- + +#### `getUpcomingBookings(userId: string): Promise` + +Returns all CONFIRMED bookings for dates >= today, ordered by date. + +--- + +## 7. Data Flow Diagrams + +### 7.1 Slot Generation Flow + +``` +Daily 02:00 UTC + โ†“ +SchedulerService.handleSlotGeneration() + โ†“ +SlotGeneratorService.generateSlots(14) + โ†“ +1. Query WeekTemplate (isActive=true) +2. For next 14 days: + - Match templates by ISO weekday + - Create TimeSlot entries +3. Use createMany(skipDuplicates: true) + โ†“ +Database: Insert new TimeSlot records + โ†“ +Return: count of new slots +``` + +--- + +### 7.2 Booking Flow + +``` +User Action + โ†“ +POST /booking + timeSlotId: UUID + membershipId: UUID + โ†“ +BookingService.createBooking() + โ†“ +START TRANSACTION + โ”œโ”€ Validate TimeSlot (status=OPEN) + โ”œโ”€ Check unique(userId, timeSlotId) + โ”œโ”€ Validate Membership (ACTIVE, not expired) + โ”œโ”€ CREATE Booking(CONFIRMED) + โ”œโ”€ UPDATE TimeSlot(bookedCount++, status=?) + โ””โ”€ UPDATE Membership(remainingTimes--) +COMMIT + โ†“ +Return: BookingWithRelations +``` + +--- + +### 7.3 Cancellation Flow + +``` +User Action + โ†“ +PUT /booking/:id/cancel + โ†“ +BookingService.cancelBooking() + โ†“ +Check: Now vs Slot Time + cancelHoursLimit + โ†“ +START TRANSACTION + โ”œโ”€ UPDATE Booking(CANCELLED, cancelledAt=NOW) + โ”œโ”€ UPDATE TimeSlot(bookedCount--, status=?) + โ””โ”€ IF within cancel window: + โ””โ”€ UPDATE Membership(remainingTimes++) +COMMIT + โ†“ +Return: { booking, refunded: boolean } +``` + +--- + +## 8. DTOs & Request/Response + +### 8.1 Time Slot DTOs + +**Location:** `packages/server/src/time-slot/dto/` + +#### `QuerySlotsDto` +```typescript +class QuerySlotsDto { + @IsDateString() + date!: string // Format: YYYY-MM-DD +} +``` + +#### `CreateManualSlotDto` +```typescript +class CreateManualSlotDto { + @IsDateString() + date!: string + @IsString() + startTime!: string + @IsString() + endTime!: string + @IsOptional() + @IsInt() + @Min(1) + capacity?: number +} +``` + +#### `WeekTemplateItemDto` & `UpdateWeekTemplateDto` +```typescript +class WeekTemplateItemDto { + @IsInt() + @Min(1) + @Max(7) + dayOfWeek!: number // ISO: 1=Mon, 7=Sun + @IsString() + startTime!: string + @IsString() + endTime!: string + @IsOptional() + capacity?: number + @IsOptional() + isActive?: boolean +} + +class UpdateWeekTemplateDto { + @ArrayNotEmpty() + templates!: WeekTemplateItemDto[] +} +``` + +--- + +## 9. Shared Constants & Enums + +**Location:** `packages/shared/src/` + +### 9.1 Constants + +```typescript +// constants.ts +export const DEFAULT_CANCEL_HOURS_LIMIT = 2 +export const DEFAULT_SLOT_CAPACITY = 1 +export const SLOT_GENERATION_DAYS = 14 +export const TIME_PERIODS = { + MORNING: { label: 'ไธŠๅˆ', start: '06:00', end: '12:00' }, + AFTERNOON: { label: 'ไธ‹ๅˆ', start: '12:00', end: '18:00' }, + EVENING: { label: 'ๆ™šไธŠ', start: '18:00', end: '22:00' }, +} +export const DATE_SELECTOR_DAYS = 7 +export const WEEKDAY_LABELS = ['', 'ๅ‘จไธ€', 'ๅ‘จไบŒ', 'ๅ‘จไธ‰', 'ๅ‘จๅ››', 'ๅ‘จไบ”', 'ๅ‘จๅ…ญ', 'ๅ‘จๆ—ฅ'] +``` + +### 9.2 Enums + +```typescript +// enums.ts +enum TimeSlotStatus { + OPEN = 'OPEN', + FULL = 'FULL', + CLOSED = 'CLOSED', +} + +enum TimeSlotSource { + TEMPLATE = 'TEMPLATE', + MANUAL = 'MANUAL', +} + +enum BookingStatus { + CONFIRMED = 'CONFIRMED', + CANCELLED = 'CANCELLED', + COMPLETED = 'COMPLETED', + NO_SHOW = 'NO_SHOW', +} + +enum MembershipStatus { + ACTIVE = 'ACTIVE', + EXPIRED = 'EXPIRED', + USED_UP = 'USED_UP', +} +``` + +--- + +## 10. File Structure Summary + +``` +packages/server/src/ +โ”œโ”€โ”€ time-slot/ +โ”‚ โ”œโ”€โ”€ __tests__/ +โ”‚ โ”‚ โ”œโ”€โ”€ slot-generator.service.spec.ts (170 lines, comprehensive tests) +โ”‚ โ”‚ โ””โ”€โ”€ time-slot.service.spec.ts +โ”‚ โ”œโ”€โ”€ dto/ +โ”‚ โ”‚ โ”œโ”€โ”€ query-slots.dto.ts +โ”‚ โ”‚ โ”œโ”€โ”€ create-manual-slot.dto.ts +โ”‚ โ”‚ โ””โ”€โ”€ week-template.dto.ts +โ”‚ โ”œโ”€โ”€ slot-generator.service.ts (172 lines, 4 key methods) +โ”‚ โ”œโ”€โ”€ time-slot.service.ts (142 lines) +โ”‚ โ”œโ”€โ”€ time-slot.controller.ts (93 lines, 2 controllers) +โ”‚ โ””โ”€โ”€ time-slot.module.ts +โ”‚ +โ”œโ”€โ”€ scheduler/ +โ”‚ โ”œโ”€โ”€ __tests__/ +โ”‚ โ”‚ โ””โ”€โ”€ scheduler.service.spec.ts +โ”‚ โ”œโ”€โ”€ scheduler.service.ts (55 lines, 4 cron jobs) +โ”‚ โ””โ”€โ”€ scheduler.module.ts +โ”‚ +โ”œโ”€โ”€ booking/ +โ”‚ โ”œโ”€โ”€ __tests__/ +โ”‚ โ”‚ โ””โ”€โ”€ booking.service.spec.ts +โ”‚ โ”œโ”€โ”€ dto/ +โ”‚ โ”‚ โ””โ”€โ”€ create-booking.dto.ts +โ”‚ โ”œโ”€โ”€ booking.service.ts (367 lines) +โ”‚ โ”œโ”€โ”€ booking.controller.ts (82 lines) +โ”‚ โ””โ”€โ”€ booking.module.ts +โ”‚ +โ”œโ”€โ”€ prisma/ +โ”‚ โ””โ”€โ”€ schema.prisma (205 lines, includes models) +โ”‚ +โ””โ”€โ”€ app.module.ts (imports TimeSlotModule, SchedulerModule) + +packages/shared/src/ +โ”œโ”€โ”€ types/ +โ”‚ โ”œโ”€โ”€ time-slot.ts +โ”‚ โ””โ”€โ”€ (others) +โ”œโ”€โ”€ constants.ts (22 lines) +โ”œโ”€โ”€ enums.ts (47 lines) +โ””โ”€โ”€ index.ts +``` + +--- + +## 11. Key Architectural Patterns + +### 11.1 Idempotent Slot Generation + +**Problem:** If scheduler crashes or delays, slots might not be generated. +**Solution:** +- Use `createMany(skipDuplicates: true)` with unique constraint on `[date, startTime, endTime]` +- Safe to re-run multiple times +- Only inserts new combinations + +--- + +### 11.2 Atomic Transactions + +**For Booking Creation:** +- Create booking, update slot, update membership in single transaction +- All-or-nothing: ensures consistency if any step fails + +**For Cancellation:** +- Cancel booking, restore slot, conditionally restore membership +- Prevents race conditions + +--- + +### 11.3 ISO Weekday Mapping + +**Problem:** JavaScript `Date.getDay()` uses 0=Sunday, but WeekTemplate uses ISO 8601 (1=Monday). + +**Solution:** Helper function `toIsoWeekday()`: +```typescript +function toIsoWeekday(jsDay: number): number { + return jsDay === 0 ? 7 : jsDay +} +``` + +--- + +### 11.4 Membership Type Handling + +**TIMES/TRIAL cardType:** +- Deduct `remainingTimes--` on booking +- Mark USED_UP when remainingTimes = 0 +- Refund if cancelled within window + +**DURATION cardType:** +- Check `expireDate` not passed +- No deduction; just check validity +- No refund on cancellation + +--- + +## 12. Example Scenarios + +### Scenario 1: Setup Studio with Mon-Fri Classes + +**Admin Actions:** +```json +PUT /admin/week-template +{ + "templates": [ + { "dayOfWeek": 1, "startTime": "09:00", "endTime": "10:00", "capacity": 1 }, + { "dayOfWeek": 1, "startTime": "10:30", "endTime": "11:30", "capacity": 1 }, + { "dayOfWeek": 2, "startTime": "09:00", "endTime": "10:00", "capacity": 1 }, + { "dayOfWeek": 3, "startTime": "09:00", "endTime": "10:00", "capacity": 1 }, + { "dayOfWeek": 4, "startTime": "09:00", "endTime": "10:00", "capacity": 1 }, + { "dayOfWeek": 5, "startTime": "09:00", "endTime": "10:00", "capacity": 1 }, + { "dayOfWeek": 5, "startTime": "18:00", "endTime": "19:00", "capacity": 1 } + ] +} +``` + +**Next Day (02:00 UTC):** +- Scheduler auto-generates 14 days of slots +- Result: 14 Mon morning + 14 Mon mid-morning + 14 Tue morning + ... + 14 Fri evening + +**Member Action (View Availability):** +``` +GET /time-slot/available?date=2026-04-10 +โ†’ Returns all slots for April 10 (Friday) +โ†’ Includes bookings for current user +``` + +--- + +### Scenario 2: Member Books, Then Cancels + +**Member Books:** +``` +POST /booking +{ + "timeSlotId": "slot-123", + "membershipId": "mem-456" +} +``` + +**System:** +1. Validates slot is OPEN, membership is ACTIVE with remaining sessions +2. Creates Booking(CONFIRMED) +3. Increments slot.bookedCount (1 โ†’ 2) +4. If now at capacity, sets slot.status = FULL +5. Decrements membership.remainingTimes (5 โ†’ 4) + +**Member Cancels (within 2-hour window):** +``` +PUT /booking/booking-789/cancel +``` + +**System:** +1. Checks if NOW + 2 hours โ‰ค slot start time โœ“ +2. Sets booking.status = CANCELLED +3. Decrements slot.bookedCount (2 โ†’ 1) +4. If slot was FULL, restores to OPEN +5. Increments membership.remainingTimes (4 โ†’ 5) โœ“ refunded + +--- + +### Scenario 3: Membership Expires + +**Overnight at 03:00 UTC:** +- Scheduler runs `handleCheckMemberships()` +- Updates all ACTIVE memberships where `expireDate < NOW` to EXPIRED +- User tries to book โ†’ BadRequestException "Membership is not active (status: EXPIRED)" + +--- + +## 13. Testing Guide + +### Key Test Files + +1. **`slot-generator.service.spec.ts`** (310 lines) + - Tests slot generation from templates + - Tests weekday mapping (JS vs ISO) + - Tests cleanup and expiration logic + - Tests membership and booking expiration + +2. **`time-slot.service.spec.ts`** (existing) + - Tests getAvailableSlots with user booking status + - Tests manual slot creation + +3. **`booking.service.spec.ts`** (existing) + - Tests booking creation with all validations + - Tests cancellation with refund logic + +--- + +## 14. Configuration & Environment + +### Required Env Variables +``` +DATABASE_URL=mysql://... +``` + +### Studio Config (StudioConfig table) +- `cancelHoursLimit`: Hours before slot to allow free cancellation (default 2) + +### Constants (shared package) +- `SLOT_GENERATION_DAYS`: 14 (days ahead to generate) +- `DEFAULT_SLOT_CAPACITY`: 1 (private lessons) +- `DEFAULT_CANCEL_HOURS_LIMIT`: 2 + +--- + +## 15. Performance Considerations + +### Database Indexes +- `TimeSlot(date)` - for date range queries +- `TimeSlot(status)` - for status filtering +- `Booking(userId)` - for user's bookings +- `Booking(status)` - for status filtering + +### Batch Operations +- Slot generation uses `createMany()` for efficiency +- Expiration checks use `updateMany()` instead of loops + +### Transaction Isolation +- All booking/cancellation operations wrapped in transactions +- Prevents race conditions on bookedCount and remainingTimes + +--- + +## 16. Security Notes + +### Authorization +- JWT guard on all endpoints +- RolesGuard for admin endpoints (only ADMIN role) +- Users can only modify their own bookings/memberships + +### Validation +- All DTOs have class-validator decorators +- UUID validation on foreign keys +- Date string validation (YYYY-MM-DD format) + +### Data Integrity +- Unique constraint on `[userId, timeSlotId]` prevents duplicate bookings +- Unique constraint on `[date, startTime, endTime]` prevents duplicate slots +- Foreign key constraints on relations + +--- + +## 17. Future Enhancement Ideas + +1. **Overbooking Buffer:** + - Allow configurable overbooking ratio (e.g., 110% capacity) + +2. **Waitlist Support:** + - Add BookingStatus.WAITLISTED + - Auto-promote when slot opens + +3. **Recurring Cancellation:** + - Cancel all future bookings of a series + - Batch refunds + +4. **Slot Availability Notifications:** + - Alert users when slots available + - Implement notification queue + +5. **Dynamic Pricing:** + - Peak vs off-peak pricing + - Last-minute discounts + +--- + +## Summary + +This time-slot and scheduling system is well-architected with: + +โœ… **Idempotent slot generation** - Safe to re-run +โœ… **Atomic transactions** - ACID compliance for bookings +โœ… **Automatic maintenance** - 4 daily cron jobs +โœ… **Flexible membership types** - TIMES, DURATION, TRIAL +โœ… **Refund policy** - Configurable cancellation window +โœ… **ISO weekday standard** - Proper international support +โœ… **Comprehensive validation** - DTOs with decorators +โœ… **Role-based access** - Admin vs member endpoints + +The system handles: +- Auto-generating 14 days of slots nightly +- Accepting bookings with capacity management +- Canceling with conditional refunds +- Expiring memberships and marking past bookings +- All with transactional integrity and concurrent safety. + diff --git a/packages/app/src/components/QuickEntry.vue b/packages/app/src/components/QuickEntry.vue index 1e8c052..2370531 100644 --- a/packages/app/src/components/QuickEntry.vue +++ b/packages/app/src/components/QuickEntry.vue @@ -106,6 +106,8 @@ async function handleLogin() { try { await userStore.login() await userStore.fetchMemberships() + // ็™ปๅฝ•ๆˆๅŠŸๅŽ่ทณ่ฝฌๅˆฐไธชไบบไธญๅฟƒ๏ผŒ่ฎฉ็”จๆˆทๅฎŒๅ–„ไฟกๆฏ + uni.navigateTo({ url: '/pages/profile/info' }) } catch { uni.showToast({ title: '็™ปๅฝ•ๅคฑ่ดฅ๏ผŒ่ฏท้‡่ฏ•', icon: 'none' }) } finally { diff --git a/packages/app/src/components/UserCard.vue b/packages/app/src/components/UserCard.vue index ba64ad4..760677c 100644 --- a/packages/app/src/components/UserCard.vue +++ b/packages/app/src/components/UserCard.vue @@ -72,7 +72,7 @@ + + diff --git a/packages/app/src/pages/admin/week-template.vue b/packages/app/src/pages/admin/week-template.vue index d883ea1..601f3d6 100644 --- a/packages/app/src/pages/admin/week-template.vue +++ b/packages/app/src/pages/admin/week-template.vue @@ -163,9 +163,9 @@ const dayOptions = [1, 2, 3, 4, 5, 6, 7].map((d) => ({ label: WEEKDAY_LABELS[d], const form = ref({ dayIdx: 0, - startTime: '09:00', - endTime: '10:00', - capacityStr: '10', + startTime: '08:00', + endTime: '09:00', + capacityStr: '1', }) const grouped = computed(() => { @@ -180,11 +180,38 @@ const grouped = computed(() => { ) }) +/** ็”Ÿๆˆ้ป˜่ฎคๆจกๆฟ๏ผšๅ‘จไธ€ๅˆฐๅ‘จๆ—ฅ๏ผŒ8:00-22:00 ๆฏๅฐๆ—ถไธ€ไธชๆ—ถๆฎต */ +function generateDefaultTemplates(): LocalTemplate[] { + const defaults: LocalTemplate[] = [] + for (let day = 1; day <= 7; day++) { + for (let hour = 8; hour < 22; hour++) { + const start = String(hour).padStart(2, '0') + ':00' + const end = String(hour + 1).padStart(2, '0') + ':00' + defaults.push({ + _key: `default-${day}-${start}`, + dayOfWeek: day, + startTime: start, + endTime: end, + capacity: 1, + isActive: true, + }) + } + } + return defaults +} + async function fetchTemplates() { loading.value = true try { - templates.value = await adminStore.fetchWeekTemplates() - isDirty.value = false + const data = await adminStore.fetchWeekTemplates() + if (data.length === 0) { + // No templates yet โ€” pre-fill with defaults + templates.value = generateDefaultTemplates() + isDirty.value = true + } else { + templates.value = data + isDirty.value = false + } } catch { uni.showToast({ title: 'ๅŠ ่ฝฝๅคฑ่ดฅ', icon: 'none' }) } finally { @@ -194,7 +221,7 @@ async function fetchTemplates() { function openAdd() { editTarget.value = null - form.value = { dayIdx: 0, startTime: '09:00', endTime: '10:00', capacityStr: '10' } + form.value = { dayIdx: 0, startTime: '08:00', endTime: '09:00', capacityStr: '1' } showModal.value = true } diff --git a/packages/app/src/pages/profile/info.vue b/packages/app/src/pages/profile/info.vue index 143ce87..d81a3e9 100644 --- a/packages/app/src/pages/profile/info.vue +++ b/packages/app/src/pages/profile/info.vue @@ -5,9 +5,9 @@