Files
oysqn.app/app/pages/admin/config.vue

283 lines
8.9 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">
<IonMenuButton />
</IonButtons>
<IonTitle>Booking Rules</IonTitle>
<IonButtons slot="end">
<IonButton :disabled="!dirty || saving" @click="saveAll">
<IonSpinner v-if="saving" name="crescent" slot="icon-only" />
<IonIcon v-else slot="icon-only" :icon="saveOutline" />
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent class="ion-padding">
<div v-if="loading" class="ion-text-center ion-padding">
<IonSpinner name="crescent" />
</div>
<template v-else>
<IonCard>
<IonCardHeader>
<IonCardTitle>Weekly Limits</IonCardTitle>
</IonCardHeader>
<IonCardContent>
<IonList lines="full">
<IonItem>
<IonLabel>
<h3>Max sessions per week</h3>
<p>Pre-booked sessions allowed per member per ISO week (MonSun)</p>
</IonLabel>
<IonInput
slot="end"
type="number"
:value="local.max_sessions_per_week"
class="config-input"
:min="1"
:max="14"
@ion-input="set('max_sessions_per_week', $event.detail.value)"
/>
</IonItem>
</IonList>
</IonCardContent>
</IonCard>
<IonCard>
<IonCardHeader>
<IonCardTitle>Weekend / Holiday Limits</IonCardTitle>
</IonCardHeader>
<IonCardContent>
<IonList lines="full">
<IonItem>
<IonLabel>
<h3>Max weekend sessions per period</h3>
<p>Max Sat/Sun/holiday pre-bookings per alternating period</p>
</IonLabel>
<IonInput
slot="end"
type="number"
:value="local.max_weekend_sessions_per_period"
class="config-input"
:min="1"
:max="10"
@ion-input="set('max_weekend_sessions_per_period', $event.detail.value)"
/>
</IonItem>
<IonItem>
<IonLabel>
<h3>Weekend period length (weeks)</h3>
<p>Number of weeks in each alternating period (default: 2 = every other weekend)</p>
</IonLabel>
<IonInput
slot="end"
type="number"
:value="local.weekend_period_weeks"
class="config-input"
:min="1"
:max="8"
@ion-input="set('weekend_period_weeks', $event.detail.value)"
/>
</IonItem>
</IonList>
</IonCardContent>
</IonCard>
<IonCard>
<IonCardHeader>
<IonCardTitle>Open Session Window</IonCardTitle>
</IonCardHeader>
<IonCardContent>
<IonList lines="full">
<IonItem>
<IonLabel>
<h3>Advance hours for open sessions</h3>
<p>Pre-booking limits are waived for sessions starting within this many hours</p>
</IonLabel>
<IonInput
slot="end"
type="number"
:value="local.open_session_advance_hours"
class="config-input"
:min="1"
:max="72"
@ion-input="set('open_session_advance_hours', $event.detail.value)"
/>
</IonItem>
</IonList>
</IonCardContent>
</IonCard>
<IonCard>
<IonCardHeader>
<IonCardTitle>Holidays</IonCardTitle>
<IonCardSubtitle>Counted as weekend days for booking limits</IonCardSubtitle>
</IonCardHeader>
<IonCardContent>
<IonList lines="full">
<IonItem v-for="h in holidays" :key="h.date">
<IonLabel>
<h3>{{ h.name }}</h3>
<p>{{ h.date }}</p>
</IonLabel>
<IonButton slot="end" fill="clear" color="danger" @click="deleteHoliday(h.date)">
<IonIcon slot="icon-only" :icon="trashOutline" />
</IonButton>
</IonItem>
</IonList>
<div class="add-holiday-row">
<IonInput v-model="newHoliday.date" type="date" placeholder="Date" class="holiday-date-input" />
<IonInput v-model="newHoliday.name" placeholder="Name (e.g. Canada Day)" class="holiday-name-input" />
<IonButton @click="addHoliday" :disabled="!newHoliday.date || !newHoliday.name">
<IonIcon slot="icon-only" :icon="addOutline" />
</IonButton>
</div>
</IonCardContent>
</IonCard>
</template>
<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,
IonMenuButton, IonButton, IonIcon, IonCard, IonCardHeader, IonCardTitle,
IonCardSubtitle, IonCardContent, IonList, IonItem, IonLabel, IonInput,
IonSpinner, IonToast, onIonViewWillEnter,
} from '@ionic/vue'
import { saveOutline, trashOutline, addOutline } from 'ionicons/icons'
import type { Database } from '~/types/supabase'
type ConfigKey = 'max_sessions_per_week' | 'max_weekend_sessions_per_period' | 'weekend_period_weeks' | 'open_session_advance_hours'
type Holiday = { date: string; name: string }
definePageMeta({ layout: false, middleware: ['auth'] })
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const supabase = useSupabaseClient() as any
const loading = ref(true)
const saving = ref(false)
const dirty = ref(false)
const toast = reactive({ show: false, message: '', color: 'success' })
const holidays = ref<Holiday[]>([])
const newHoliday = reactive({ date: '', name: '' })
const local = reactive<Record<ConfigKey, number>>({
max_sessions_per_week: 2,
max_weekend_sessions_per_period: 1,
weekend_period_weeks: 2,
open_session_advance_hours: 24,
})
const original = reactive<Record<ConfigKey, number>>({ ...local })
async function loadAll() {
await Promise.all([fetchConfig(), fetchHolidays()])
loading.value = false
}
const user = useSupabaseUser()
watch(user, (val) => { if (val) loadAll() }, { immediate: true })
onIonViewWillEnter(() => { if (user.value) loadAll() })
async function fetchConfig() {
const { data } = await supabase
.from('booking_config')
.select('key, value')
for (const row of (data ?? []) as { key: string; value: unknown }[]) {
const k = row.key as ConfigKey
if (k in local) {
local[k] = Number(row.value)
original[k] = Number(row.value)
}
}
}
async function fetchHolidays() {
const { data } = await supabase
.from('holidays')
.select('date, name')
.order('date', { ascending: true })
holidays.value = (data ?? []) as Holiday[]
}
function set(key: ConfigKey, value: string | number | null | undefined) {
const n = Number(value)
if (!isNaN(n) && n > 0) {
local[key] = n
dirty.value = true
}
}
async function saveAll() {
saving.value = true
const upserts = (Object.keys(local) as ConfigKey[]).map(k => ({ key: k, value: local[k] }))
const { error } = await supabase
.from('booking_config')
.upsert(upserts, { onConflict: 'key' })
saving.value = false
if (error) { showToast('Save failed: ' + error.message, 'danger'); return }
Object.assign(original, local)
dirty.value = false
showToast('Booking rules saved', 'success')
}
async function addHoliday() {
const { error } = await supabase
.from('holidays')
.insert({ date: newHoliday.date, name: newHoliday.name })
if (error) { showToast('Failed: ' + error.message, 'danger'); return }
newHoliday.date = ''
newHoliday.name = ''
await fetchHolidays()
showToast('Holiday added', 'success')
}
async function deleteHoliday(date: string) {
const { error } = await supabase
.from('holidays')
.delete()
.eq('date', date)
if (error) { showToast('Failed: ' + error.message, 'danger'); return }
await fetchHolidays()
showToast('Holiday removed', 'success')
}
function showToast(message: string, color: string) {
toast.message = message
toast.color = color
toast.show = true
}
</script>
<style scoped>
.config-input {
text-align: right;
max-width: 80px;
}
.add-holiday-row {
display: flex;
gap: 0.5rem;
align-items: center;
margin-top: 0.75rem;
}
.holiday-date-input { flex: 0 0 160px; }
.holiday-name-input { flex: 1; }
</style>