feat: Schedule bookings

This commit is contained in:
2026-04-21 16:12:04 -04:00
parent 108c042921
commit 5b4955f07e
9 changed files with 1636 additions and 4 deletions

View File

@@ -7,6 +7,9 @@
</IonButtons>
<IonTitle>Manage Slots</IonTitle>
<IonButtons slot="end">
<IonButton @click="showWeekApply = true">
<IonIcon slot="icon-only" :icon="calendarOutline" />
</IonButton>
<IonButton @click="showAddSlot = true">
<IonIcon slot="icon-only" :icon="addOutline" />
</IonButton>
@@ -127,6 +130,52 @@
</IonContent>
</IonModal>
<!-- Apply to Week modal -->
<IonModal :is-open="showWeekApply" @did-dismiss="showWeekApply = false">
<IonHeader>
<IonToolbar>
<IonTitle>Apply Template to Week</IonTitle>
<IonButtons slot="end">
<IonButton @click="showWeekApply = false">Cancel</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent class="ion-padding">
<IonList lines="full">
<IonItem>
<IonLabel position="stacked">Template</IonLabel>
<IonSelect v-model="weekApply.templateId" placeholder="Select template" interface="action-sheet">
<IonSelectOption v-for="t in templates" :key="t.id" :value="t.id">{{ t.name }}</IonSelectOption>
</IonSelect>
</IonItem>
<IonItem>
<IonLabel position="stacked">Boat</IonLabel>
<IonSelect v-model="weekApply.boatId" placeholder="Select boat" interface="action-sheet">
<IonSelectOption v-for="b in boats" :key="b.id" :value="b.id">{{ b.display_name || b.name }}</IonSelectOption>
</IonSelect>
</IonItem>
</IonList>
<h4 class="section-title ion-margin-top">Days to apply</h4>
<IonList lines="none">
<IonItem v-for="(day, i) in weekDays" :key="day.iso">
<IonCheckbox v-model="weekApply.days[i]" slot="start" />
<IonLabel>{{ day.label }}</IonLabel>
</IonItem>
</IonList>
<IonButton
expand="block"
class="ion-margin-top"
:disabled="!weekApply.templateId || !weekApply.boatId || !weekApply.days.some(Boolean) || weekApply.applying"
@click="applyTemplateToWeek"
>
<IonSpinner v-if="weekApply.applying" name="crescent" slot="start" />
Apply to Selected Days
</IonButton>
</IonContent>
</IonModal>
<IonToast
v-model:is-open="toast.show"
:message="toast.message"
@@ -143,10 +192,10 @@ import {
IonPage, IonHeader, IonToolbar, IonTitle, IonContent, IonButtons,
IonMenuButton, IonButton, IonIcon, IonCard, IonCardHeader, IonCardTitle,
IonCardSubtitle, IonCardContent, IonList, IonItem, IonLabel, IonToggle,
IonSelect, IonSelectOption, IonSpinner, IonModal, IonInput, IonToast,
IonSelect, IonSelectOption, IonSpinner, IonModal, IonInput, IonToast, IonCheckbox,
} from '@ionic/vue'
import {
addOutline, timeOutline, trashOutline, constructOutline,
addOutline, timeOutline, trashOutline, constructOutline, calendarOutline,
} from 'ionicons/icons'
import type { Database, TimeTuple } from '~/types/supabase'
@@ -162,6 +211,23 @@ const supabase = useSupabaseClient() as any
const loading = ref(false)
const addingSlot = ref(false)
const showAddSlot = ref(false)
const showWeekApply = ref(false)
const weekApply = reactive({ templateId: '', boatId: '', applying: false, days: [] as boolean[] })
watch(showWeekApply, (open) => {
if (open) weekApply.days = Array(7).fill(true)
})
const weekDays = computed(() => {
const base = new Date(selectedDate.value + 'T12:00:00')
const dow = base.getDay() // 0=Sun
const monday = new Date(base)
monday.setDate(base.getDate() - ((dow + 6) % 7))
return Array.from({ length: 7 }, (_, i) => {
const d = new Date(monday)
d.setDate(monday.getDate() + i)
return { iso: d.toISOString().slice(0, 10), label: d.toLocaleDateString('en-CA', { weekday: 'short', month: 'short', day: 'numeric' }) }
})
})
const selectedDate = ref(todayIso())
const boats = ref<Boat[]>([])
const templates = ref<Template[]>([])
@@ -280,6 +346,28 @@ async function applyTemplate(boatId: string, templateId: string) {
showToast(`Template "${template.name}" applied`, 'success')
}
async function applyTemplateToWeek() {
const template = templates.value.find(t => t.id === weekApply.templateId)
if (!template) return
weekApply.applying = true
const selectedDays = weekDays.value.filter((_, i) => weekApply.days[i])
const inserts = selectedDays.flatMap(day =>
(template.time_tuples as TimeTuple[]).map(([start, end]) => ({
boat_id: weekApply.boatId,
start_time: new Date(`${day.iso}T${start}:00`).toISOString(),
end_time: new Date(`${day.iso}T${end}:00`).toISOString(),
}))
)
const { error } = await supabase.from('intervals').insert(inserts)
weekApply.applying = false
if (error) { showToast('Failed to apply: ' + error.message, 'danger'); return }
showWeekApply.value = false
await fetchSlots()
showToast(`Template applied to ${selectedDays.length} day(s)`, 'success')
}
function showToast(message: string, color: string) {
toast.message = message
toast.color = color