## 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>
297 lines
7.9 KiB
Markdown
297 lines
7.9 KiB
Markdown
# 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<Record<number, LocalTemplate[]>> // 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 <token>` 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)
|
|
|