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:
richarjiang
2026-04-05 12:18:49 +08:00
parent 9c5dd4a911
commit b6986ba30c
29 changed files with 7810 additions and 19 deletions

View 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