Files
mp-pilates/packages/server/src/time-slot/time-slot.controller.ts
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

107 lines
2.9 KiB
TypeScript

import {
Controller,
Get,
Post,
Put,
Param,
Body,
Query,
UseGuards,
HttpCode,
HttpStatus,
} from '@nestjs/common'
import { JwtAuthGuard } from '../auth/jwt-auth.guard'
import { RolesGuard } from '../auth/roles.guard'
import { Roles } from '../auth/roles.decorator'
import { CurrentUser } from '../common/decorators/current-user.decorator'
import { UserRole } from '@mp-pilates/shared'
import { TimeSlotService } from './time-slot.service'
import { SlotGeneratorService } from './slot-generator.service'
import { QuerySlotsDto } from './dto/query-slots.dto'
import { CreateManualSlotDto } from './dto/create-manual-slot.dto'
import { UpdateWeekTemplateDto } from './dto/week-template.dto'
import { PublishDaySlotsDto } from './dto/publish-day-slots.dto'
// ---------------------------------------------------------------------------
// Member endpoints
// ---------------------------------------------------------------------------
@UseGuards(JwtAuthGuard)
@Controller('time-slot')
export class TimeSlotController {
constructor(private readonly timeSlotService: TimeSlotService) {}
@Get('available')
getAvailableSlots(
@Query() query: QuerySlotsDto,
@CurrentUser('sub') userId: string,
) {
return this.timeSlotService.getAvailableSlots(query.date, userId)
}
@Get(':id')
getSlotById(@Param('id') id: string) {
return this.timeSlotService.getSlotById(id)
}
}
// ---------------------------------------------------------------------------
// Admin endpoints
// ---------------------------------------------------------------------------
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN)
@Controller('admin')
export class AdminTimeSlotController {
constructor(
private readonly timeSlotService: TimeSlotService,
private readonly slotGeneratorService: SlotGeneratorService,
) {}
// Week template management
@Get('week-template')
getWeekTemplates() {
return this.timeSlotService.getWeekTemplates()
}
@Put('week-template')
replaceWeekTemplates(@Body() dto: UpdateWeekTemplateDto) {
return this.timeSlotService.replaceWeekTemplates(dto.templates)
}
// Manual slot management
@Post('time-slot/manual')
createManualSlot(@Body() dto: CreateManualSlotDto) {
return this.timeSlotService.createManualSlot(dto)
}
@Put('time-slot/:id/close')
@HttpCode(HttpStatus.OK)
closeSlot(@Param('id') id: string) {
return this.timeSlotService.closeSlot(id)
}
// Slot generation trigger
@Post('generate-slots')
@HttpCode(HttpStatus.OK)
generateSlots() {
return this.slotGeneratorService.generateSlots()
}
// Schedule preview & publish
@Get('schedule/preview')
getSchedulePreview(@Query('date') date: string) {
return this.timeSlotService.getSchedulePreview(date)
}
@Post('schedule/publish')
@HttpCode(HttpStatus.OK)
publishDaySlots(@Body() dto: PublishDaySlotsDto) {
return this.timeSlotService.publishDaySlots(dto)
}
}