monacousa-portal/components/CreateEventDialog.vue

472 lines
13 KiB
Vue
Raw Normal View History

<template>
<v-dialog v-model="show" max-width="800" persistent>
<v-card>
<v-card-title class="d-flex justify-space-between align-center">
<div class="d-flex align-center">
<v-icon class="me-2">mdi-calendar-plus</v-icon>
<span>Create New Event</span>
</div>
<v-btn
@click="close"
icon
variant="text"
size="small"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text>
<v-form ref="form" v-model="valid" @submit.prevent="handleSubmit">
<v-row>
<!-- Basic Information -->
<v-col cols="12">
<v-text-field
v-model="eventData.title"
label="Event Title*"
:rules="[v => !!v || 'Title is required']"
variant="outlined"
required
/>
</v-col>
<v-col cols="12">
<v-textarea
v-model="eventData.description"
label="Description"
variant="outlined"
rows="3"
auto-grow
/>
</v-col>
<!-- Event Type and Visibility -->
<v-col cols="12" md="6">
<v-select
v-model="eventData.event_type"
:items="eventTypes"
label="Event Type*"
:rules="[v => !!v || 'Event type is required']"
variant="outlined"
required
/>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="eventData.visibility"
:items="visibilityOptions"
label="Visibility*"
:rules="[v => !!v || 'Visibility is required']"
variant="outlined"
required
/>
</v-col>
<!-- Date and Time -->
<v-col cols="12" md="6">
<v-text-field
v-model="eventData.start_datetime"
label="Start Date & Time*"
type="datetime-local"
:rules="[v => !!v || 'Start date is required']"
variant="outlined"
required
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="eventData.end_datetime"
label="End Date & Time*"
type="datetime-local"
:rules="[
v => !!v || 'End date is required',
v => !eventData.start_datetime || new Date(v) > new Date(eventData.start_datetime) || 'End date must be after start date'
]"
variant="outlined"
required
/>
</v-col>
<!-- Location -->
<v-col cols="12">
<v-text-field
v-model="eventData.location"
label="Location"
variant="outlined"
/>
</v-col>
<!-- Capacity Settings -->
<v-col cols="12" md="6">
<v-text-field
v-model="eventData.max_attendees"
label="Maximum Attendees"
type="number"
variant="outlined"
hint="Leave empty for unlimited capacity"
persistent-hint
/>
</v-col>
<!-- Payment Settings -->
<v-col cols="12" md="6">
<v-switch
v-model="isPaidEvent"
label="Paid Event"
color="primary"
inset
/>
</v-col>
<!-- Payment Details (shown when paid event) -->
<template v-if="isPaidEvent">
<v-col cols="12" md="6">
<v-text-field
v-model="eventData.cost_members"
label="Cost for Members (€)"
type="number"
step="0.01"
variant="outlined"
:rules="isPaidEvent ? [v => !!v || 'Member cost is required'] : []"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="eventData.cost_non_members"
label="Cost for Non-Members (€)"
type="number"
step="0.01"
variant="outlined"
:rules="isPaidEvent ? [v => !!v || 'Non-member cost is required'] : []"
/>
</v-col>
<v-col cols="12">
<v-switch
v-model="memberPricingEnabled"
label="Enable Member Pricing"
color="primary"
inset
hint="Allow current members to pay member rates"
persistent-hint
/>
</v-col>
</template>
<!-- Advanced Options -->
<v-col cols="12">
<v-expansion-panels variant="accordion">
<v-expansion-panel>
<v-expansion-panel-title>
<v-icon start>mdi-cog</v-icon>
Advanced Options
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-row>
<v-col cols="12" md="6">
<v-switch
v-model="isRecurring"
label="Recurring Event"
color="primary"
inset
hint="Create a series of events"
persistent-hint
/>
</v-col>
<v-col v-if="isRecurring" cols="12" md="6">
<v-select
v-model="recurrenceFrequency"
:items="recurrenceOptions"
label="Frequency"
variant="outlined"
/>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="eventData.status"
:items="statusOptions"
label="Status"
variant="outlined"
/>
</v-col>
</v-row>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-col>
</v-row>
</v-form>
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer />
<v-btn
@click="close"
variant="outlined"
:disabled="loading"
>
Cancel
</v-btn>
<v-btn
@click="handleSubmit"
color="primary"
:loading="loading"
:disabled="!valid"
>
Create Event
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import type { EventCreateRequest } from '~/utils/types';
import { useAuth } from '~/composables/useAuth';
import { useEvents } from '~/composables/useEvents';
interface Props {
modelValue: boolean;
prefilledDate?: string;
prefilledEndDate?: string;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
prefilledDate: undefined,
prefilledEndDate: undefined
});
const emit = defineEmits<{
'update:modelValue': [value: boolean];
'event-created': [event: any];
}>();
const { isAdmin } = useAuth();
const { createEvent } = useEvents();
// Reactive state
const form = ref();
const valid = ref(false);
const loading = ref(false);
const isPaidEvent = ref(false);
const memberPricingEnabled = ref(true);
const isRecurring = ref(false);
const recurrenceFrequency = ref('weekly');
// Form data
const eventData = reactive<EventCreateRequest>({
title: '',
description: '',
event_type: 'social',
start_datetime: '',
end_datetime: '',
location: '',
max_attendees: '',
is_paid: 'false',
cost_members: '',
cost_non_members: '',
member_pricing_enabled: 'true',
visibility: 'public',
status: 'active'
});
// Computed
const show = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
});
// Options
const eventTypes = [
{ title: 'Social Event', value: 'social' },
{ title: 'Meeting', value: 'meeting' },
{ title: 'Fundraiser', value: 'fundraiser' },
{ title: 'Workshop', value: 'workshop' },
{ title: 'Board Only', value: 'board-only' }
];
const visibilityOptions = computed(() => {
const options = [
{ title: 'Public', value: 'public' },
{ title: 'Board Only', value: 'board-only' }
];
if (isAdmin.value) {
options.push({ title: 'Admin Only', value: 'admin-only' });
}
return options;
});
const statusOptions = [
{ title: 'Active', value: 'active' },
{ title: 'Draft', value: 'draft' }
];
const recurrenceOptions = [
{ title: 'Weekly', value: 'weekly' },
{ title: 'Monthly', value: 'monthly' },
{ title: 'Yearly', value: 'yearly' }
];
// Watchers
watch(isPaidEvent, (newValue) => {
eventData.is_paid = newValue ? 'true' : 'false';
});
watch(memberPricingEnabled, (newValue) => {
eventData.member_pricing_enabled = newValue ? 'true' : 'false';
});
watch(isRecurring, (newValue) => {
eventData.is_recurring = newValue ? 'true' : 'false';
if (newValue) {
eventData.recurrence_pattern = JSON.stringify({
frequency: recurrenceFrequency.value,
interval: 1,
end_date: null
});
} else {
eventData.recurrence_pattern = '';
}
});
watch(recurrenceFrequency, (newValue) => {
if (isRecurring.value) {
eventData.recurrence_pattern = JSON.stringify({
frequency: newValue,
interval: 1,
end_date: null
});
}
});
// Watch for prefilled dates
watch(() => props.prefilledDate, (newDate) => {
if (newDate) {
eventData.start_datetime = newDate;
// Set end date 2 hours later if not provided
if (!props.prefilledEndDate) {
const endDate = new Date(newDate);
endDate.setHours(endDate.getHours() + 2);
eventData.end_datetime = endDate.toISOString().slice(0, 16);
}
}
}, { immediate: true });
watch(() => props.prefilledEndDate, (newEndDate) => {
if (newEndDate) {
eventData.end_datetime = newEndDate;
}
}, { immediate: true });
// Methods
const resetForm = () => {
eventData.title = '';
eventData.description = '';
eventData.event_type = 'social';
eventData.start_datetime = '';
eventData.end_datetime = '';
eventData.location = '';
eventData.max_attendees = '';
eventData.is_paid = 'false';
eventData.cost_members = '';
eventData.cost_non_members = '';
eventData.member_pricing_enabled = 'true';
eventData.visibility = 'public';
eventData.status = 'active';
eventData.is_recurring = 'false';
eventData.recurrence_pattern = '';
isPaidEvent.value = false;
memberPricingEnabled.value = true;
isRecurring.value = false;
recurrenceFrequency.value = 'weekly';
form.value?.resetValidation();
};
const close = () => {
show.value = false;
resetForm();
};
const handleSubmit = async () => {
if (!form.value) return;
const isValid = await form.value.validate();
if (!isValid.valid) return;
loading.value = true;
try {
// Ensure datetime strings are properly formatted
const startDate = new Date(eventData.start_datetime);
const endDate = new Date(eventData.end_datetime);
const formattedEventData = {
...eventData,
start_datetime: startDate.toISOString(),
end_datetime: endDate.toISOString()
};
const newEvent = await createEvent(formattedEventData);
emit('event-created', newEvent);
// Show success message
// TODO: Add toast/snackbar notification
console.log('Event created successfully:', newEvent);
close();
} catch (error: any) {
console.error('Error creating event:', error);
// TODO: Add error toast/snackbar notification
} finally {
loading.value = false;
}
};
// Initialize form when dialog opens
watch(show, (isOpen) => {
if (isOpen && props.prefilledDate) {
eventData.start_datetime = props.prefilledDate;
if (props.prefilledEndDate) {
eventData.end_datetime = props.prefilledEndDate;
} else {
// Set end date 2 hours later
const endDate = new Date(props.prefilledDate);
endDate.setHours(endDate.getHours() + 2);
eventData.end_datetime = endDate.toISOString().slice(0, 16);
}
}
});
</script>
<style scoped>
.v-card {
max-height: 90vh;
overflow-y: auto;
}
.v-expansion-panel-title {
font-weight: 500;
}
.v-switch {
flex: 0 0 auto;
}
.v-text-field :deep(.v-field__input) {
min-height: 56px;
}
</style>