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) <noreply@anthropic.com>
This commit is contained in:
803
ADMIN_SCHEDULING_EXPLORATION.md
Normal file
803
ADMIN_SCHEDULING_EXPLORATION.md
Normal file
@@ -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<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
|
||||
|
||||
Reference in New Issue
Block a user