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

265 lines
8.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">
<IonMenuButton />
</IonButtons>
<IonTitle>Interval Templates</IonTitle>
<IonButtons slot="end">
<IonButton @click="openCreate">
<IonIcon slot="icon-only" :icon="addOutline" />
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent class="ion-padding">
<div v-if="loading" class="ion-text-center ion-padding">
<IonSpinner name="crescent" />
</div>
<p v-else-if="templates.length === 0" class="empty-text">No templates yet. Create one to get started.</p>
<IonCard v-else v-for="t in templates" :key="t.id" class="template-card">
<IonCardHeader>
<div class="template-header">
<IonCardTitle>{{ t.name }}</IonCardTitle>
<div class="header-actions">
<IonButton fill="clear" @click="openEdit(t)">
<IonIcon slot="icon-only" :icon="pencilOutline" />
</IonButton>
<IonButton fill="clear" color="danger" @click="confirmDelete(t)">
<IonIcon slot="icon-only" :icon="trashOutline" />
</IonButton>
</div>
</div>
</IonCardHeader>
<IonCardContent>
<div class="time-tuple-list">
<span v-for="(tuple, i) in (t.time_tuples as TimeTuple[])" :key="i" class="time-tuple-chip">
{{ tuple[0] }}{{ tuple[1] }}
</span>
</div>
</IonCardContent>
</IonCard>
<!-- Create/Edit modal -->
<IonModal :is-open="showModal" @did-dismiss="closeModal">
<IonHeader>
<IonToolbar>
<IonTitle>{{ editing ? 'Edit Template' : 'New Template' }}</IonTitle>
<IonButtons slot="end">
<IonButton @click="closeModal">Cancel</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent class="ion-padding">
<IonList lines="full">
<IonItem>
<IonLabel position="stacked">Template Name</IonLabel>
<IonInput v-model="form.name" placeholder="e.g. Weekday Standard" />
</IonItem>
</IonList>
<h4 class="section-title ion-margin-top">Time Slots</h4>
<div
v-for="(tuple, i) in form.tuples"
:key="i"
class="tuple-row"
>
<IonInput
class="time-input"
type="time"
:value="tuple[0]"
@ion-input="tuple[0] = ($event.detail.value as string)"
/>
<span class="tuple-dash"></span>
<IonInput
class="time-input"
type="time"
:value="tuple[1]"
@ion-input="tuple[1] = ($event.detail.value as string)"
/>
<IonButton fill="clear" color="danger" @click="removeTuple(i)">
<IonIcon slot="icon-only" :icon="closeOutline" />
</IonButton>
</div>
<IonButton expand="block" fill="outline" class="ion-margin-top" @click="addTuple">
<IonIcon slot="start" :icon="addOutline" />
Add Time Slot
</IonButton>
<IonButton
expand="block"
class="ion-margin-top"
:disabled="!form.name || form.tuples.length === 0 || saving"
@click="saveTemplate"
>
<IonSpinner v-if="saving" name="crescent" slot="start" />
Save Template
</IonButton>
</IonContent>
</IonModal>
<IonAlert
:is-open="deleteAlert.show"
header="Delete Template"
:message="`Delete '${deleteAlert.name}'? This cannot be undone.`"
:buttons="[
{ text: 'Cancel', role: 'cancel', handler: () => { deleteAlert.show = false } },
{ text: 'Delete', role: 'destructive', handler: () => deleteTemplate() },
]"
@did-dismiss="deleteAlert.show = false"
/>
<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,
IonCardContent, IonList, IonItem, IonLabel, IonInput, IonSpinner,
IonModal, IonAlert, IonToast, onIonViewWillEnter,
} from '@ionic/vue'
import { addOutline, pencilOutline, trashOutline, closeOutline } from 'ionicons/icons'
import type { Database, TimeTuple } from '~/types/supabase'
type Template = Database['public']['Tables']['interval_templates']['Row']
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 showModal = ref(false)
const editing = ref<Template | null>(null)
const templates = ref<Template[]>([])
const toast = reactive({ show: false, message: '', color: 'success' })
const deleteAlert = reactive({ show: false, id: '', name: '' })
const form = reactive<{ name: string; tuples: [string, string][] }>({
name: '',
tuples: [['08:00', '12:00']],
})
const user = useSupabaseUser()
watch(user, (val) => { if (val) fetchTemplates() }, { immediate: true })
onIonViewWillEnter(() => { if (user.value) fetchTemplates() })
async function fetchTemplates() {
loading.value = true
const { data } = await supabase.from('interval_templates').select('*').order('name')
templates.value = data ?? []
loading.value = false
}
function openCreate() {
editing.value = null
form.name = ''
form.tuples = [['08:00', '12:00']]
showModal.value = true
}
function openEdit(t: Template) {
editing.value = t
form.name = t.name
form.tuples = (t.time_tuples as TimeTuple[]).map(tuple => [tuple[0], tuple[1]])
showModal.value = true
}
function closeModal() {
showModal.value = false
editing.value = null
}
function addTuple() {
form.tuples.push(['13:00', '17:00'])
}
function removeTuple(i: number) {
form.tuples.splice(i, 1)
}
async function saveTemplate() {
saving.value = true
const payload = {
name: form.name,
time_tuples: form.tuples as TimeTuple[],
}
let error
if (editing.value) {
;({ error } = await supabase.from('interval_templates').update(payload).eq('id', editing.value.id))
} else {
;({ error } = await supabase.from('interval_templates').insert(payload))
}
saving.value = false
if (error) { showToast('Save failed: ' + error.message, 'danger'); return }
closeModal()
await fetchTemplates()
showToast(editing.value ? 'Template updated' : 'Template created', 'success')
}
function confirmDelete(t: Template) {
deleteAlert.id = t.id
deleteAlert.name = t.name
deleteAlert.show = true
}
async function deleteTemplate() {
const { error } = await supabase.from('interval_templates').delete().eq('id', deleteAlert.id)
deleteAlert.show = false
if (error) { showToast('Delete failed: ' + error.message, 'danger'); return }
await fetchTemplates()
showToast('Template deleted', 'success')
}
function showToast(message: string, color: string) {
toast.message = message
toast.color = color
toast.show = true
}
</script>
<style scoped>
.template-card { margin: 0 0 1rem; }
.template-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-actions { display: flex; }
.time-tuple-list { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.time-tuple-chip {
padding: 0.3rem 0.75rem;
border-radius: 1rem;
background: var(--ion-color-light);
border: 1px solid var(--ion-color-light-shade);
font-size: 0.85rem;
font-weight: 500;
}
.section-title { font-size: 0.95rem; font-weight: 600; margin: 0 0 0.5rem; }
.tuple-row {
display: flex;
align-items: center;
gap: 0.4rem;
margin-bottom: 0.5rem;
}
.time-input { flex: 1; }
.tuple-dash { font-weight: 600; }
.empty-text { color: var(--ion-color-medium); text-align: center; padding: 2rem 0; }
</style>