## 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) <noreply@anthropic.com>
804 lines
24 KiB
Markdown
804 lines
24 KiB
Markdown
# 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<AdminStats>({ 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<LocalTemplate[]>([])
|
||
const loading = ref(false)
|
||
const saving = ref(false)
|
||
const isDirty = ref(false) // Tracks unsaved changes
|
||
const showModal = ref(false)
|
||
const editTarget = ref<LocalTemplate | null>(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<TimeSlot[]>([])
|
||
|
||
// 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<WeekTemplate[]>
|
||
// GET /admin/week-template
|
||
// Returns all templates for current studio
|
||
// Usage: Gets templates for display in week-template.vue
|
||
|
||
async saveWeekTemplates(templates: WeekTemplateInput[]): Promise<WeekTemplate[]>
|
||
// 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<TimeSlot[]>
|
||
// 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<TimeSlot>
|
||
// POST /admin/time-slot/manual
|
||
// Creates a one-off time slot
|
||
// Used in slot-adjust.vue Tab 0
|
||
|
||
async closeSlot(id: string): Promise<TimeSlot>
|
||
// 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<AdminStats>
|
||
// 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<WeekTemplate[]>
|
||
// Returns all templates sorted by day/time
|
||
|
||
async replaceWeekTemplates(items: Array<{...}>): Promise<any>
|
||
// Transaction-based replacement:
|
||
// 1. Delete all existing templates
|
||
// 2. Create new ones from items array
|
||
// 3. Return count of created
|
||
|
||
async createManualSlot(dto): Promise<TimeSlot>
|
||
// Creates slot with source=MANUAL, status=OPEN
|
||
|
||
async closeSlot(id: string): Promise<TimeSlot>
|
||
// 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<number>
|
||
// 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<number>
|
||
// Called by scheduler
|
||
// Closes all OPEN slots with date < today
|
||
|
||
async checkExpiredMemberships(): Promise<number>
|
||
// Called by scheduler
|
||
// Expires memberships past end date or with 0 sessions left
|
||
|
||
async completeBookings(): Promise<number>
|
||
// 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<T>(options: RequestOptions): Promise<T>
|
||
// Makes HTTP request with JWT auth
|
||
// Auto-refreshes token on 401
|
||
|
||
function get<T>(url: string, data?: Record<string, unknown>): Promise<T>
|
||
function post<T>(url: string, data?: Record<string, unknown>): Promise<T>
|
||
function put<T>(url: string, data?: Record<string, unknown>): Promise<T>
|
||
function del<T>(url: string, data?: Record<string, unknown>): Promise<T>
|
||
|
||
// 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 <token>`
|
||
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
|
||
|