Files
milsim-site-v4/ui/src/components/calendar/CreateCalendarEvent.vue
2025-11-28 01:01:14 -05:00

365 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 + "T20:00:00");
start = 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 scrollbar-themed" 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>
<style scoped>
/* Firefox */
.scrollbar-themed {
scrollbar-width: thin;
scrollbar-color: #555 #1f1f1f;
padding-right: 6px;
}
/* Chrome, Edge, Safari */
.scrollbar-themed::-webkit-scrollbar {
width: 10px;
/* slightly wider to allow padding look */
}
.scrollbar-themed::-webkit-scrollbar-track {
background: #1f1f1f;
margin-left: 6px;
/* ❗ adds space between content + scrollbar */
}
.scrollbar-themed::-webkit-scrollbar-thumb {
background: #555;
border-radius: 9999px;
}
.scrollbar-themed::-webkit-scrollbar-thumb:hover {
background: #777;
}
</style>