336 lines
13 KiB
Vue
336 lines
13 KiB
Vue
<script setup lang="ts">
|
|
import { toTypedSchema } from "@vee-validate/zod"
|
|
import { ref, defineExpose, watch, nextTick } from "vue"
|
|
import * as z from "zod"
|
|
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog"
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from "@/components/ui/form"
|
|
import { Input } from "@/components/ui/input"
|
|
import Textarea from "../ui/textarea/Textarea.vue"
|
|
import { CalendarEvent } from "@shared/types/calendar"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectGroup,
|
|
SelectItem,
|
|
SelectLabel,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select'
|
|
|
|
|
|
|
|
function toLocalDateString(d: Date) {
|
|
// yyyy-MM-dd with local time zone
|
|
const y = d.getFullYear()
|
|
const m = String(d.getMonth() + 1).padStart(2, "0")
|
|
const day = String(d.getDate()).padStart(2, "0")
|
|
return `${y}-${m}-${day}`
|
|
}
|
|
function toLocalTimeString(d: Date) {
|
|
const hh = String(d.getHours()).padStart(2, "0")
|
|
const mm = String(d.getMinutes()).padStart(2, "0")
|
|
return `${hh}:${mm}`
|
|
}
|
|
function roundToNextHour(d = new Date()) {
|
|
const t = new Date(d)
|
|
t.setMinutes(0, 0, 0)
|
|
t.setHours(t.getHours() + 1)
|
|
return t
|
|
}
|
|
|
|
import { calendarEventSchema, parseLocalDateTime } from '@shared/schemas/calendarEventSchema'
|
|
import { createCalendarEvent, editCalendarEvent } from "@/api/calendar"
|
|
import DialogDescription from "../ui/dialog/DialogDescription.vue"
|
|
const formSchema = toTypedSchema(calendarEventSchema)
|
|
|
|
// ---------- dialog state & defaults ----------
|
|
const clickedDate = ref<string | null>(null);
|
|
const dialogOpen = ref(false)
|
|
const dialogMode = ref<'create' | 'edit'>('create');
|
|
const editEvent = ref<CalendarEvent | null>();
|
|
function openDialog(dateStr?: string, mode?: 'create' | 'edit', event?: CalendarEvent) {
|
|
dialogMode.value = mode ?? 'create';
|
|
editEvent.value = event ?? null;
|
|
clickedDate.value = dateStr ?? null;
|
|
dialogOpen.value = true
|
|
initialValues.value = makeInitialValues()
|
|
}
|
|
defineExpose({ openDialog })
|
|
|
|
function makeInitialValues() {
|
|
|
|
if (dialogMode.value === 'edit' && editEvent.value) {
|
|
const e = editEvent.value;
|
|
return {
|
|
name: e.name,
|
|
startDate: toLocalDateString(new Date(e.start)),
|
|
startTime: toLocalTimeString(new Date(e.start)),
|
|
endDate: toLocalDateString(new Date(e.end)),
|
|
endTime: toLocalTimeString(new Date(e.end)),
|
|
location: e.location,
|
|
color: e.color,
|
|
description: e.description,
|
|
id: e.id,
|
|
}
|
|
}
|
|
|
|
let start: Date;
|
|
if (clickedDate.value) {
|
|
const local = new Date(clickedDate.value + "T00:00:00");
|
|
start = roundToNextHour(local);
|
|
} else {
|
|
start = roundToNextHour();
|
|
}
|
|
const end = new Date(start.getTime() + 60 * 60 * 1000)
|
|
return {
|
|
name: "",
|
|
startDate: toLocalDateString(start),
|
|
startTime: toLocalTimeString(start),
|
|
endDate: toLocalDateString(end),
|
|
endTime: toLocalTimeString(end),
|
|
location: "",
|
|
color: "#6890ee",
|
|
description: "",
|
|
id: null as number | null,
|
|
}
|
|
}
|
|
const initialValues = ref(null)
|
|
|
|
const formKey = ref(0)
|
|
|
|
watch(dialogOpen, async (isOpen) => {
|
|
if (isOpen) {
|
|
await nextTick();
|
|
formRef.value?.resetForm({ values: makeInitialValues() })
|
|
}
|
|
})
|
|
// ---------- submit ----------
|
|
async function onSubmit(vals: z.infer<typeof calendarEventSchema>) {
|
|
const start = parseLocalDateTime(vals.startDate, vals.startTime)
|
|
const end = parseLocalDateTime(vals.endDate, vals.endTime)
|
|
|
|
const event: CalendarEvent = {
|
|
id: vals.id ?? null,
|
|
name: vals.name,
|
|
start,
|
|
end,
|
|
location: vals.location,
|
|
color: vals.color,
|
|
description: vals.description,
|
|
}
|
|
|
|
try {
|
|
if (dialogMode.value === "edit") {
|
|
await editCalendarEvent(event);
|
|
} else {
|
|
await createCalendarEvent(event);
|
|
}
|
|
emit('reload');
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
|
|
// close after success
|
|
dialogOpen.value = false
|
|
}
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'reload'): void
|
|
}>()
|
|
const formRef = ref(null);
|
|
|
|
const colorOptions = [
|
|
{ name: "Blue", hex: "#6890ee" },
|
|
{ name: "Purple", hex: "#a25fce" },
|
|
{ name: "Orange", hex: "#fba037" },
|
|
{ name: "Green", hex: "#6cd265" },
|
|
{ name: "Red", hex: "#ff5959" },
|
|
];
|
|
|
|
function getColorName(hex: string) {
|
|
return colorOptions.find(c => c.hex === hex)?.name ?? hex
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Form ref="formRef" :key="formKey" v-slot="{ handleSubmit, resetForm }" :validation-schema="formSchema"
|
|
:initial-values="initialValues" keep-values as="">
|
|
<Dialog v-model:open="dialogOpen">
|
|
<DialogContent class="sm:max-w-[520px]">
|
|
<DialogHeader>
|
|
<DialogTitle>{{ dialogMode == "edit" ? 'Edit Event' : 'Create Event' }}</DialogTitle>
|
|
<DialogDescription></DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<form id="dialogForm" class="grid grid-cols-1 gap-4"
|
|
@submit="handleSubmit($event, (vals) => { onSubmit(vals); resetForm({ values: initialValues }); })">
|
|
|
|
<div class="flex gap-3 items-start w-full">
|
|
<!-- Name -->
|
|
<div class="flex-1">
|
|
<FormField v-slot="{ componentField }" name="name">
|
|
<FormItem>
|
|
<FormLabel>Event Name</FormLabel>
|
|
<FormControl>
|
|
<Input type="text" v-bind="componentField" />
|
|
</FormControl>
|
|
<div class="h-3">
|
|
<FormMessage />
|
|
</div>
|
|
</FormItem>
|
|
</FormField>
|
|
</div>
|
|
|
|
<!-- Color -->
|
|
<div class="w-[120px]">
|
|
<FormField v-slot="{ componentField }" name="color">
|
|
<FormItem>
|
|
<FormLabel>Color</FormLabel>
|
|
<FormControl>
|
|
<Select :modelValue="componentField.modelValue"
|
|
@update:modelValue="componentField.onChange">
|
|
<SelectTrigger>
|
|
<SelectValue asChild>
|
|
<template #default="{ selected }">
|
|
<div class="flex items-center gap-2 w-[70px]">
|
|
<span class="inline-block size-4 rounded"
|
|
:style="{ background: componentField.modelValue }">
|
|
</span>
|
|
<span>{{ getColorName(componentField.modelValue) }}</span>
|
|
</div>
|
|
</template>
|
|
</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem v-for="opt in colorOptions" :key="opt.hex" :value="opt.hex">
|
|
<div class="flex items-center gap-2">
|
|
<span class="inline-block size-4 rounded"
|
|
:style="{ background: opt.hex }"></span>
|
|
<span>{{ opt.name }}</span>
|
|
</div>
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
</FormControl>
|
|
<div class="h-3">
|
|
<FormMessage />
|
|
</div>
|
|
</FormItem>
|
|
</FormField>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Start: date + time -->
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<FormField v-slot="{ componentField }" name="startDate">
|
|
<FormItem>
|
|
<FormLabel>Start Date</FormLabel>
|
|
<FormControl>
|
|
<Input type="date" v-bind="componentField" />
|
|
</FormControl>
|
|
<div class="h-3">
|
|
<FormMessage />
|
|
</div>
|
|
</FormItem>
|
|
</FormField>
|
|
|
|
<FormField v-slot="{ componentField }" name="startTime">
|
|
<FormItem>
|
|
<FormLabel>Start Time</FormLabel>
|
|
<FormControl>
|
|
<Input type="text" v-bind="componentField" />
|
|
<!-- If you ever want native picker: type="time" -->
|
|
</FormControl>
|
|
<div class="h-3">
|
|
<FormMessage />
|
|
</div>
|
|
</FormItem>
|
|
</FormField>
|
|
</div>
|
|
|
|
<!-- End: date + time -->
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<FormField v-slot="{ componentField }" name="endDate">
|
|
<FormItem>
|
|
<FormLabel>End Date</FormLabel>
|
|
<FormControl>
|
|
<Input type="date" v-bind="componentField" />
|
|
</FormControl>
|
|
<div class="h-3">
|
|
<FormMessage />
|
|
</div>
|
|
</FormItem>
|
|
</FormField>
|
|
|
|
<FormField v-slot="{ componentField }" name="endTime">
|
|
<FormItem>
|
|
<FormLabel>End Time</FormLabel>
|
|
<FormControl>
|
|
<Input type="text" v-bind="componentField" />
|
|
</FormControl>
|
|
<div class="h-3">
|
|
<FormMessage />
|
|
</div>
|
|
</FormItem>
|
|
</FormField>
|
|
</div>
|
|
|
|
<!-- Location -->
|
|
<FormField v-slot="{ componentField }" name="location">
|
|
<FormItem>
|
|
<FormLabel>Location</FormLabel>
|
|
<FormControl>
|
|
<Input type="text" v-bind="componentField" />
|
|
</FormControl>
|
|
<div class="h-3">
|
|
<FormMessage />
|
|
</div>
|
|
</FormItem>
|
|
</FormField>
|
|
|
|
|
|
<!-- Description -->
|
|
<FormField v-slot="{ componentField }" name="description">
|
|
<FormItem>
|
|
<FormLabel>Description</FormLabel>
|
|
<FormControl>
|
|
<Textarea class="resize-none h-32" v-bind="componentField" />
|
|
</FormControl>
|
|
<div class="h-3">
|
|
<FormMessage />
|
|
</div>
|
|
</FormItem>
|
|
</FormField>
|
|
|
|
<!-- Hidden id -->
|
|
<FormField v-slot="{ componentField }" name="id">
|
|
<input type="hidden" v-bind="componentField" />
|
|
</FormField>
|
|
</form>
|
|
|
|
<DialogFooter>
|
|
<Button type="submit" form="dialogForm">{{ dialogMode == "edit" ? 'Update' : 'Create' }}</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</Form>
|
|
</template>
|