# Admin Scheduling - Quick Reference Guide ## 🎯 Quick Links to Key Files ### Frontend Components | File | Lines | Purpose | |------|-------|---------| | `packages/app/src/pages/admin/index.vue` | 1-177 | Admin dashboard, 6 nav items | | `packages/app/src/pages/admin/week-template.vue` | 1-500 | **MAIN: Schedule template management** | | `packages/app/src/pages/admin/slot-adjust.vue` | 1-428 | 3 tabs: add/close/generate slots | | `packages/app/src/stores/admin.ts` | 1-171 | API calls (Pinia store) | ### Backend Services | File | Purpose | |------|---------| | `packages/server/src/time-slot/time-slot.controller.ts` | API endpoints (/admin/*) | | `packages/server/src/time-slot/time-slot.service.ts` | Template & slot logic | | `packages/server/src/time-slot/slot-generator.service.ts` | Auto-generate slots from templates | | `packages/server/src/time-slot/dto/week-template.dto.ts` | Input validation | ### Shared Types & Constants | File | Exports | |------|---------| | `packages/shared/src/types/week-template.ts` | `WeekTemplate`, `WeekTemplateInput` | | `packages/shared/src/types/time-slot.ts` | `TimeSlot`, `CreateManualSlotDto` | | `packages/shared/src/constants.ts` | `WEEKDAY_LABELS`, `SLOT_GENERATION_DAYS`, etc. | --- ## 🔄 The Flow: In 30 Seconds ``` Admin edits templates ↓ isDirty = true → Save bar appears ↓ Admin taps "保存全部更改" ↓ PUT /admin/week-template (full array) ↓ Backend: DELETE all, CREATE new (atomic) ↓ Scheduler triggers (nightly or manual) ↓ POST /admin/generate-slots ↓ SlotGeneratorService fetches active templates ↓ For each day (next 14 days): Match templates by ISO weekday Create TimeSlot records (source=TEMPLATE) ↓ Members see slots and can book ``` --- ## 📊 Core Entities ### WeekTemplate (Database) ```typescript id: string // UUID dayOfWeek: number // 1=Mon, 2=Tue, ..., 7=Sun startTime: string // "09:00" endTime: string // "10:00" capacity: number // Max bookings isActive: boolean // Enabled/disabled createdAt: string updatedAt: string ``` ### TimeSlot (Database) ```typescript id: string date: string // YYYY-MM-DD startTime: string endTime: string capacity: number bookedCount: number // How many booked status: "OPEN" | "FULL" | "CLOSED" source: "TEMPLATE" | "MANUAL" templateId: string | null // Links to WeekTemplate createdAt: string updatedAt: string ``` --- ## 🌐 API Endpoints ### GET /admin/week-template Returns all templates (ordered by dayOfWeek ASC, startTime ASC) ```json [ { "id": "uuid1", "dayOfWeek": 1, "startTime": "09:00", "endTime": "10:00", "capacity": 10, "isActive": true, "createdAt": "2026-04-05T00:00:00Z", "updatedAt": "2026-04-05T00:00:00Z" } ] ``` ### PUT /admin/week-template Replace all templates (atomic transaction) ```json { "templates": [ { "dayOfWeek": 1, "startTime": "09:00", "endTime": "10:00", "capacity": 10, "isActive": true }, { "dayOfWeek": 1, "startTime": "18:00", "endTime": "19:00", "capacity": 8, "isActive": true }, { "dayOfWeek": 3, "startTime": "10:00", "endTime": "11:00", "capacity": 12, "isActive": false } ] } ``` ### POST /admin/time-slot/manual Create a one-off slot ```json { "date": "2026-04-15", "startTime": "14:00", "endTime": "15:00", "capacity": 10 } ``` ### PUT /admin/time-slot/:id/close Close a slot (changes status to CLOSED) ### POST /admin/generate-slots Generate slots for next 14 days from active templates Response: `{ "count": 28 }` --- ## 🎨 UI State Management ### week-template.vue Local State ```typescript // Main data templates: LocalTemplate[] // All templates grouped: Computed> // By dayOfWeek // UI states loading: boolean // Initial fetch saving: boolean // Save in progress isDirty: boolean // Show save bar? showModal: boolean // Show add/edit modal? editTarget: LocalTemplate | null // Editing which template? // Modal form form: { dayIdx: number // 0-6 (picker index) startTime: string // "09:00" endTime: string // "10:00" capacityStr: string // User input as string } ``` ### Key Computed ```typescript const grouped = computed(() => { // Groups templates by dayOfWeek for rendering // Sorts by day number ascending (1-7) // Returns: { 1: [...], 3: [...], 5: [...], ... } }) ``` --- ## 🔐 Permissions & Auth All `/admin/*` endpoints require: 1. Valid JWT token in `Authorization: Bearer ` header 2. User role must be `UserRole.ADMIN` 3. Guards: `@UseGuards(JwtAuthGuard, RolesGuard)` --- ## 🧮 Important Constants From `packages/shared/src/constants.ts`: ```typescript SLOT_GENERATION_DAYS = 14 // Generate 14 days ahead DEFAULT_SLOT_CAPACITY = 1 // Private lesson default DEFAULT_CANCEL_HOURS_LIMIT = 2 // Cancel up to 2 hours before WEEKDAY_LABELS = [ '', // index 0 (unused) '周一', // index 1 → dayOfWeek 1 (Monday) '周二', // index 2 → dayOfWeek 2 '周三', // ... etc '周四', '周五', '周六', '周日' // index 7 → dayOfWeek 7 (Sunday) ] ``` --- ## 🐛 Common Gotchas ### 1. dayOfWeek vs JS getDay() - **Frontend uses**: ISO weekday (1=Mon, 7=Sun) - **JS Date.getDay()**: 0=Sun, 6=Sat - **Backend converts**: `toIsoWeekday()` in slot-generator.service.ts ### 2. Template Replace (Not Merge) - `PUT /admin/week-template` **deletes all** and creates new - NOT a merge/patch operation - Frontend must send complete array ### 3. isDirty Flag - Tracks **any** change locally (add/edit/delete/toggle) - Used to show/hide save bar - Cleared after successful save ### 4. Timezone - All dates stored as UTC midnight: `setUTCHours(0,0,0,0)` - Frontend displays as local YYYY-MM-DD strings - May cause off-by-one on day boundaries ### 5. Slot Generation - Uses `skipDuplicates: true` in Prisma - Safe to re-run without creating duplicates - Assumes `date + startTime + endTime` is unique --- ## 💡 Usage Example: Add a Monday 9AM Class **Frontend (week-template.vue)**: ```typescript // User clicks "+ 新增时段" openAdd() form.value = { dayIdx: 0, startTime: '09:00', endTime: '10:00', capacityStr: '10' } showModal.value = true // User confirms in modal submitForm() templates.value.push({ _key: String(Date.now()), dayOfWeek: 1, // dayOptions[0].value = Monday startTime: '09:00', endTime: '10:00', capacity: 10, isActive: true }) isDirty.value = true // ← Save bar appears // User taps "保存全部更改" handleSave() payload = templates.value.map(t => ({...})) await adminStore.saveWeekTemplates(payload) // Backend creates transaction: // DELETE FROM week_template // INSERT INTO week_template (day_of_week, start_time, end_time, capacity, is_active) // VALUES (1, '09:00', '10:00', 10, true) // ... (all other templates) // Frontend refetches and displays ``` --- ## 🔗 Related Components - **Admin Members** (`pages/admin/members.vue`): Shows member list - **Admin Orders** (`pages/admin/orders.vue`): Shows order history - **Admin Card Types** (`pages/admin/card-types.vue`): Manage membership cards - **Admin Studio** (`pages/admin/studio.vue`): Studio info settings --- ## 📈 Scalability Notes ### Current Approach - Templates: Small dataset (typically < 50 records) - Slots: Generated in batches (14 days at a time) - Uses `skipDuplicates` to handle reruns safely ### Bottlenecks - Template replacement deletes ALL and creates NEW (atomic but slow with 1000s) - Slot generation is serial (could be parallelized) - No pagination for templates (assumes all fit in memory) ### Future Improvements - Batch template updates (don't replace all) - Pagination if templates > 100 - Incremental slot generation (detect last generated date)