Files

212 lines
7.1 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<IonPage>
<IonHeader>
<IonToolbar color="primary">
<IonButtons slot="start">
<IonBackButton default-href="/" />
</IonButtons>
<IonTitle>Edit Reservation</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent class="ion-padding">
<div v-if="loading" class="ion-text-center ion-padding">
<IonSpinner name="crescent" />
</div>
<template v-else-if="reservation">
<IonCard>
<IonCardHeader>
<IonCardTitle>{{ boatName }}</IonCardTitle>
</IonCardHeader>
<IonCardContent>
<div class="time-row">
<IonIcon :icon="timeOutline" />
<span>{{ formatDateRange(reservation.start_time, reservation.end_time) }}</span>
</div>
</IonCardContent>
</IonCard>
<IonList lines="full" class="ion-margin-top">
<IonItem v-if="authStore.isAdmin">
<IonLabel position="stacked">Member</IonLabel>
<IonSelect v-model="form.user_id" interface="action-sheet">
<IonSelectOption v-for="m in members" :key="m.user_id" :value="m.user_id">
{{ m.first_name }} {{ m.last_name }}
</IonSelectOption>
</IonSelect>
</IonItem>
<IonItem>
<IonLabel position="stacked">Reason</IonLabel>
<IonSelect v-model="form.reason" placeholder="Select reason" interface="action-sheet">
<IonSelectOption v-for="r in reasonOptions" :key="r" :value="r">{{ r }}</IonSelectOption>
</IonSelect>
</IonItem>
<IonItem>
<IonLabel position="stacked">Additional Comments (optional)</IonLabel>
<IonTextarea v-model="form.comment" :rows="3" placeholder="Any notes..." />
</IonItem>
</IonList>
<div class="form-actions">
<IonButton fill="outline" @click="router.back()">Cancel</IonButton>
<IonButton :disabled="!form.reason || saving" @click="save">
<IonSpinner v-if="saving" name="crescent" slot="start" />
Save Changes
</IonButton>
</div>
<IonButton
v-if="isFuture"
expand="block"
fill="outline"
color="danger"
class="ion-margin-top"
:disabled="saving"
@click="confirmCancel"
>
<IonIcon slot="start" :icon="trashOutline" />
Cancel Reservation
</IonButton>
</template>
<p v-else class="empty-text">Reservation not found.</p>
<IonToast
v-model:is-open="toast.show"
:message="toast.message"
:color="toast.color"
:duration="2500"
position="bottom"
/>
</IonContent>
</IonPage>
</template>
<script setup lang="ts">
import {
IonPage, IonHeader, IonToolbar, IonTitle, IonContent, IonButtons,
IonBackButton, IonCard, IonCardHeader, IonCardTitle, IonCardContent,
IonList, IonItem, IonLabel, IonSelect, IonSelectOption, IonTextarea,
IonButton, IonSpinner, IonIcon, IonToast, alertController, onIonViewWillEnter,
} from '@ionic/vue'
import { useRoute, useRouter } from 'vue-router'
import { timeOutline, trashOutline } from 'ionicons/icons'
import type { Database } from '~/types/supabase'
import { useAuthStore } from '~/stores/auth'
type ReservationWithBoat = Database['public']['Tables']['reservations']['Row'] & {
boats: { name: string; display_name: string | null } | null
}
type Member = Database['public']['Tables']['members']['Row']
definePageMeta({ layout: false, middleware: ['auth'] })
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const supabase = useSupabaseClient() as any
const user = useSupabaseUser()
const authStore = useAuthStore()
const route = useRoute()
const router = useRouter()
const id = computed(() => route.params.id as string)
const loading = ref(true)
const saving = ref(false)
const reservation = ref<ReservationWithBoat | null>(null)
const members = ref<Member[]>([])
const toast = reactive({ show: false, message: '', color: 'success' })
const form = reactive({ reason: '', comment: '', user_id: '' })
const reasonOptions = ['Open Sail', 'Private Sail', 'Racing', 'Training', 'Other']
const boatName = computed(() =>
reservation.value?.boats?.display_name || reservation.value?.boats?.name || 'Unknown boat'
)
const isFuture = computed(() =>
reservation.value ? new Date(reservation.value.start_time) > new Date() : false
)
async function loadReservation() {
const reservationId = route.params.id as string
if (!user.value || !reservationId) return
loading.value = true
const [{ data }, { data: memberData }] = await Promise.all([
supabase.from('reservations').select('*, boats(name, display_name)').eq('id', reservationId).single(),
authStore.isAdmin
? supabase.from('members').select('*').order('last_name')
: Promise.resolve({ data: [] }),
])
reservation.value = data ?? null
members.value = memberData ?? []
if (data) {
form.reason = data.reason ?? ''
form.comment = data.comment ?? ''
form.user_id = data.user_id ?? ''
}
loading.value = false
}
watch(user, (val) => { if (val) loadReservation() }, { immediate: true })
onIonViewWillEnter(() => { if (user.value) loadReservation() })
async function save() {
saving.value = true
const payload: Record<string, string> = { reason: form.reason, comment: form.comment }
if (authStore.isAdmin && form.user_id) payload.user_id = form.user_id
const { error } = await supabase
.from('reservations')
.update(payload)
.eq('id', id.value)
saving.value = false
if (error) {
toast.message = 'Failed to save changes.'
toast.color = 'danger'
toast.show = true
return
}
toast.message = 'Reservation updated.'
toast.color = 'success'
toast.show = true
setTimeout(() => router.back(), 1500)
}
async function confirmCancel() {
const alert = await alertController.create({
header: 'Cancel Reservation',
message: 'Are you sure you want to cancel this reservation?',
buttons: [
{ text: 'Keep it', role: 'cancel' },
{
text: 'Cancel Reservation',
role: 'destructive',
handler: () => void cancelReservation(),
},
],
})
await alert.present()
}
async function cancelReservation() {
saving.value = true
await supabase.from('reservations').update({ status: 'cancelled' }).eq('id', id.value)
saving.value = false
router.back()
}
function formatDateRange(start: string, end: string) {
const s = new Date(start)
const e = new Date(end)
const date = s.toLocaleDateString('en-CA', { weekday: 'short', month: 'short', day: 'numeric' })
const startTime = s.toLocaleTimeString('en-CA', { hour: '2-digit', minute: '2-digit' })
const endTime = e.toLocaleTimeString('en-CA', { hour: '2-digit', minute: '2-digit' })
return `${date} · ${startTime}${endTime}`
}
</script>
<style scoped>
.time-row { display: flex; align-items: center; gap: 0.5rem; font-size: 0.95rem; }
.form-actions { display: flex; gap: 0.75rem; justify-content: flex-end; margin-top: 1.5rem; }
.empty-text { color: var(--ion-color-medium); text-align: center; padding: 2rem 0; }
</style>