Files
mp-pilates/ADMIN_SCHEDULING_EXPLORATION.md
richarjiang b6986ba30c 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>
2026-04-05 12:18:49 +08:00

24 KiB
Raw Blame History

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:

- navigate(path): Navigates to admin pages
- loadStats(): Fetches dashboard statistics via adminStore.fetchDashboardStats()

State:

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

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

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

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
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
// 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

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

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

// ── 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

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:

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:

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 dashboardindex.vue

    • Taps "排课设置" nav item
  2. Admin navigates to Week Template pageweek-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

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

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

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

  • Week template CRUD (Create, Read, Update via replace)
  • Manual slot creation
  • Close individual slots
  • Batch slot generation from templates
  • UI for all three slot adjustment tabs
  • Local state change tracking (isDirty)
  • Modal form for adding/editing templates
  • Grouping templates by weekday
  • 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:
    // 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)

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