feat: Enhance reservation functionality

This commit is contained in:
2026-04-22 10:23:22 -04:00
parent 7f1e82acc2
commit 534d66c774
25 changed files with 1236 additions and 91 deletions

View File

@@ -1,13 +1,14 @@
import { createClient } from "npm:@supabase/supabase-js@2"
interface CreateReservationBody {
boat_id: string
start_time: string // ISO 8601
end_time: string // ISO 8601
reason?: string
comment?: string
member_ids?: string[]
guest_ids?: string[]
boat_id: string
start_time: string // ISO 8601
end_time: string // ISO 8601
reason?: string
comment?: string
member_ids?: string[]
guest_ids?: string[]
target_user_id?: string // admin only: create on behalf of another member
}
interface BookingConfig {
@@ -83,7 +84,7 @@ Deno.serve(async (req: Request) => {
const { data: { user }, error: authError } = await adminClient.auth.getUser(token)
if (authError || !user) return errorResponse('unauthorized', 'Invalid session', 401)
const userId = user.id
const callerId = user.id
// Parse body
let body: CreateReservationBody
@@ -93,7 +94,18 @@ Deno.serve(async (req: Request) => {
return errorResponse('invalid_body', 'Request body must be JSON', 400)
}
const { boat_id, start_time, end_time, reason = '', comment = '', member_ids = [], guest_ids = [] } = body
const { boat_id, start_time, end_time, reason = '', comment = '', member_ids = [], guest_ids = [], target_user_id } = body
// Resolve admin status once — used for limit bypass and target_user_id trust
const { data: callerMember } = await adminClient
.from('members')
.select('role')
.eq('user_id', callerId)
.single()
const isAdmin = callerMember?.role === 'admin'
// Effective user: admin may create on behalf of another member
const userId = (isAdmin && target_user_id) ? target_user_id : callerId
if (!boat_id || !start_time || !end_time) {
return errorResponse('missing_fields', 'boat_id, start_time, and end_time are required', 400)
@@ -113,15 +125,8 @@ Deno.serve(async (req: Request) => {
// ── Historical booking guard ────────────────────────────────────────────────
const isPast = endDate.getTime() < Date.now()
if (isPast) {
const { data: callerMember } = await adminClient
.from('members')
.select('role')
.eq('user_id', userId)
.single()
if (callerMember?.role !== 'admin') {
return errorResponse('historical_booking_not_allowed', 'Can not book a reservation in the past.', 422)
}
if (isPast && !isAdmin) {
return errorResponse('historical_booking_not_allowed', 'Can not book a reservation in the past.', 422)
}
// ── 1. Load booking config ──────────────────────────────────────────────────
@@ -156,7 +161,7 @@ Deno.serve(async (req: Request) => {
if (boatErr || !boat) return errorResponse('not_found', 'Boat not found', 404)
if (!boat.booking_available) return errorResponse('boat_unavailable', 'This boat is currently out of service', 422)
if (!isPast && boat.required_certs.length > 0) {
if (!isAdmin && !isPast && boat.required_certs.length > 0) {
const { data: member } = await adminClient
.from('members')
.select('certifications')
@@ -178,7 +183,7 @@ Deno.serve(async (req: Request) => {
const advanceMs = config.open_session_advance_hours * 60 * 60 * 1000
const isOpenSession = startDate.getTime() - Date.now() <= advanceMs
if (!isPast && !isOpenSession) {
if (!isAdmin && !isPast && !isOpenSession) {
// ── 5. Weekly pre-booking limit ─────────────────────────────────────────
const weekStart = isoWeekStart(startDate)
const weekEnd = new Date(weekStart.getTime() + 7 * 24 * 60 * 60 * 1000)
@@ -187,6 +192,7 @@ Deno.serve(async (req: Request) => {
.from('reservations')
.select('start_time, created_at')
.eq('user_id', userId)
.neq('status', 'cancelled')
.gte('start_time', weekStart.toISOString())
.lt('start_time', weekEnd.toISOString())
@@ -219,6 +225,7 @@ Deno.serve(async (req: Request) => {
.from('reservations')
.select('start_time, created_at')
.eq('user_id', userId)
.neq('status', 'cancelled')
.gte('start_time', periodStart.toISOString())
.lt('start_time', periodEnd.toISOString())
@@ -253,7 +260,7 @@ Deno.serve(async (req: Request) => {
comment,
member_ids,
guest_ids,
status: 'pending',
status: 'confirmed',
})
.select()
.single()