feat: Enhance reservation functionality
This commit is contained in:
211
app/pages/reservations/edit/[id].vue
Normal file
211
app/pages/reservations/edit/[id].vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user