feat:實作預約系統 — 時段管理、預約生命週期與前端整合

後端:
- 新增 course_schedules / bookings migration(含索引)
- BookingStatus / ScheduleStatus PHP BackedEnum
- CourseSchedule / Booking Model(七狀態機 VALID_TRANSITIONS)
- ScheduleController、ProviderBookingController、MemberBookingController
- 雙層名額驗證(API 層快速失敗 + DB lockForUpdate 防超賣)
- 24h 取消截止、pending 不佔位設計
- ExpirePendingBookings(每小時)/ CompleteFinishedBookings(每日)Scheduler
- Docker cron 配置、CACHE_STORE 改為 file 修正 502

前端:
- 課程詳情頁加入時段選擇與預約流程
- 我的預約頁(展開式卡片、狀態說明、連結課程詳情)
- Coach 時段管理(上午/下午時間選擇器、新課程引導)
- Coach 預約管理(依課程分組、待確認徽章)
- Navbar 新增「我的預約」與「時段/預約管理」入口
- 密碼格式提示與即時比對

OpenSpec:
- booking-system change 歸檔至 archive/2026-05-12-booking-system
- 新增 specs/course-scheduling 與 specs/booking-lifecycle 主規格

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 00:24:51 +08:00
parent ad2c05779d
commit 975b56ca54
40 changed files with 2202 additions and 18 deletions
@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('course_schedules', function (Blueprint $table) {
$table->id();
$table->foreignId('diving_offer_id')->constrained('diving_offers')->cascadeOnDelete();
$table->foreignId('provider_id')->constrained('users')->cascadeOnDelete();
$table->date('scheduled_date');
$table->time('start_time');
$table->unsignedInteger('max_participants');
$table->unsignedInteger('current_participants')->default(0);
$table->string('status')->default('open');
$table->timestamps();
$table->index(['diving_offer_id', 'status', 'scheduled_date'], 'idx_offer_status_date');
$table->index('provider_id', 'idx_provider_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('course_schedules');
}
};
@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('bookings', function (Blueprint $table) {
$table->id();
$table->foreignId('schedule_id')->constrained('course_schedules')->cascadeOnDelete();
$table->foreignId('member_id')->constrained('users')->cascadeOnDelete();
$table->unsignedInteger('participants')->default(1);
$table->unsignedInteger('total_price');
$table->string('status')->default('pending');
$table->text('notes')->nullable();
$table->timestamps();
$table->index(['member_id', 'status'], 'idx_member_status');
$table->index(['schedule_id', 'status'], 'idx_schedule_status');
$table->index(['status', 'created_at'], 'idx_status_created_at');
$table->index(['status', 'schedule_id'], 'idx_status_sched');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('bookings');
}
};