monacousa-portal/components/EventCalendar.vue

508 lines
13 KiB
Vue
Raw Permalink Normal View History

<template>
<v-card elevation="2" class="event-calendar">
<v-card-title v-if="!compact" class="d-flex justify-space-between align-center">
<div class="d-flex align-center">
<v-icon class="me-2">mdi-calendar</v-icon>
<span>Events Calendar</span>
</div>
<div v-if="showCreateButton && (isBoard || isAdmin)" class="d-flex gap-2">
<v-btn
@click="$emit('create-event')"
color="primary"
size="small"
prepend-icon="mdi-plus"
>
Create Event
</v-btn>
</div>
</v-card-title>
<v-card-text>
<!-- Mobile view selector -->
<v-row v-if="$vuetify.display.mobile && !compact" class="mb-4">
<v-col cols="12">
<v-btn-toggle
v-model="mobileView"
color="primary"
variant="outlined"
2025-08-13 15:57:34 +02:00
density="comfortable"
mandatory
2025-08-13 15:57:34 +02:00
class="w-100"
>
2025-08-13 15:57:34 +02:00
<v-btn value="week" class="flex-grow-1">
<v-icon start>mdi-calendar-week</v-icon>
Week
</v-btn>
<v-btn value="month" class="flex-grow-1">
<v-icon start>mdi-calendar-month</v-icon>
Month
</v-btn>
2025-08-13 15:57:34 +02:00
<v-btn value="list" class="flex-grow-1">
<v-icon start>mdi-format-list-bulleted</v-icon>
Agenda
</v-btn>
</v-btn-toggle>
</v-col>
</v-row>
<!-- Loading state -->
<v-skeleton-loader
v-if="loading"
type="image"
:height="calendarHeight"
class="rounded"
/>
<!-- FullCalendar component -->
<FullCalendar
v-else
ref="fullCalendar"
:options="calendarOptions"
class="fc-theme-monacousa"
/>
<!-- No events message -->
<v-alert
v-if="!loading && (!events || events.length === 0)"
type="info"
variant="tonal"
class="mt-4"
>
<v-alert-title>No Events</v-alert-title>
No events found for the current time period.
</v-alert>
</v-card-text>
</v-card>
</template>
<script setup lang="ts">
import FullCalendar from '@fullcalendar/vue3';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
import listPlugin from '@fullcalendar/list';
import type { Event, FullCalendarEvent } from '~/utils/types';
import { useAuth } from '~/composables/useAuth';
interface Props {
events?: Event[];
loading?: boolean;
compact?: boolean;
height?: number | string;
showCreateButton?: boolean;
initialView?: string;
}
const props = withDefaults(defineProps<Props>(), {
events: () => [],
loading: false,
compact: false,
height: 600,
showCreateButton: true,
initialView: 'dayGridMonth'
});
const emit = defineEmits<{
'event-click': [event: any];
'date-click': [date: any];
'view-change': [view: any];
'date-range-change': [start: string, end: string];
'create-event': [];
}>();
const { isBoard, isAdmin } = useAuth();
// Reactive state
const fullCalendar = ref<InstanceType<typeof FullCalendar>>();
2025-08-13 15:57:34 +02:00
const mobileView = ref('week'); // Default to week view on mobile
// Computed properties
const calendarHeight = computed(() => {
if (props.compact) return props.height || 300;
if (typeof props.height === 'number') return props.height;
return props.height || 600;
});
const currentView = computed(() => {
if (props.compact) return 'dayGridMonth';
// Mobile responsive view switching
if (process.client && window.innerWidth < 960) {
2025-08-13 15:57:34 +02:00
switch (mobileView.value) {
case 'week': return 'dayGridWeek';
case 'list': return 'listWeek';
case 'month':
default: return 'dayGridMonth';
}
}
return props.initialView;
});
const transformedEvents = computed((): FullCalendarEvent[] => {
2025-08-12 17:31:03 +02:00
console.log('[EventCalendar] Raw events received:', props.events.length);
2025-08-13 13:18:07 +02:00
console.log('[EventCalendar] Raw events array:', props.events);
2025-08-12 17:31:03 +02:00
props.events.forEach((event, index) => {
console.log(`[EventCalendar] Event ${index + 1}:`, {
id: event.id,
title: event.title,
start_datetime: event.start_datetime,
end_datetime: event.end_datetime,
event_type: event.event_type
});
});
const transformed = props.events.map((event: Event) => transformEventForCalendar(event));
console.log('[EventCalendar] Transformed events for FullCalendar:', transformed.length);
2025-08-13 13:18:07 +02:00
console.log('[EventCalendar] Transformed events array:', transformed);
2025-08-12 17:31:03 +02:00
transformed.forEach((event, index) => {
console.log(`[EventCalendar] Transformed Event ${index + 1}:`, {
id: event.id,
title: event.title,
start: event.start,
end: event.end,
backgroundColor: event.backgroundColor
});
});
return transformed;
});
// FullCalendar options
const calendarOptions = computed(() => ({
plugins: [dayGridPlugin, interactionPlugin, listPlugin],
initialView: currentView.value,
height: calendarHeight.value,
headerToolbar: props.compact ? false : {
left: 'prev,next today',
center: 'title',
right: process.client && window.innerWidth < 960 ?
'dayGridMonth,listWeek' :
'dayGridMonth,dayGridWeek,listWeek'
} as any,
events: transformedEvents.value,
eventClick: handleEventClick,
dateClick: handleDateClick,
datesSet: handleDatesSet,
eventDidMount: handleEventMount,
dayMaxEvents: props.compact ? 2 : 5,
eventDisplay: 'block',
displayEventTime: true,
eventTimeFormat: {
hour: '2-digit' as const,
minute: '2-digit' as const,
hour12: false
},
locale: 'en',
firstDay: 1, // Monday
weekends: true,
navLinks: true,
selectable: isBoard.value || isAdmin.value,
selectMirror: true,
select: handleDateSelect,
// Mobile optimizations
aspectRatio: process.client && window.innerWidth < 960 ? 1.0 : 1.35,
// Responsive behavior
windowResizeDelay: 100
}));
// Event handlers
function handleEventClick(clickInfo: any) {
emit('event-click', {
event: clickInfo.event,
eventData: clickInfo.event.extendedProps
});
}
function handleDateClick(dateInfo: any) {
if (isBoard.value || isAdmin.value) {
emit('date-click', {
date: dateInfo.dateStr,
allDay: dateInfo.allDay
});
}
}
function handleDateSelect(selectInfo: any) {
if (isBoard.value || isAdmin.value) {
emit('date-click', {
date: selectInfo.startStr,
endDate: selectInfo.endStr,
allDay: selectInfo.allDay
});
}
}
function handleDatesSet(dateInfo: any) {
emit('view-change', {
view: dateInfo.view.type,
start: dateInfo.start,
end: dateInfo.end
});
emit('date-range-change',
dateInfo.start.toISOString(),
dateInfo.end.toISOString()
);
}
function handleEventMount(mountInfo: any) {
// Add custom styling or tooltips
const event = mountInfo.event;
const el = mountInfo.el;
// Add tooltip with event details
el.setAttribute('title', `${event.title}\n${event.extendedProps.location || 'No location'}`);
// Add custom classes based on event properties
if (event.extendedProps.is_paid) {
el.classList.add('fc-paid-event');
}
if (event.extendedProps.user_rsvp?.rsvp_status === 'confirmed') {
el.classList.add('fc-user-rsvp');
}
}
// Transform event data for FullCalendar
function transformEventForCalendar(event: Event): FullCalendarEvent {
2025-08-13 12:27:21 +02:00
console.log('[EventCalendar] Transforming event:', {
id: event.id,
2025-08-13 13:51:27 +02:00
event_id: event.event_id,
2025-08-13 12:27:21 +02:00
title: event.title,
start_datetime: event.start_datetime,
end_datetime: event.end_datetime,
event_type: event.event_type
});
const eventTypeColors = {
'meeting': { bg: '#2196f3', border: '#1976d2' },
'social': { bg: '#4caf50', border: '#388e3c' },
'fundraiser': { bg: '#ff9800', border: '#f57c00' },
'workshop': { bg: '#9c27b0', border: '#7b1fa2' },
'board-only': { bg: '#a31515', border: '#8b1212' }
};
const colors = eventTypeColors[event.event_type] ||
{ bg: '#757575', border: '#424242' };
2025-08-13 13:51:27 +02:00
// Use event_id as the primary identifier for FullCalendar uniqueness
const calendarId = event.event_id || event.id || `temp_${(event as any).Id}_${Date.now()}`;
console.log('[EventCalendar] Using calendar ID:', calendarId, 'from event_id:', event.event_id, 'fallback id:', event.id);
2025-08-13 12:27:21 +02:00
// Ensure dates are properly formatted for FullCalendar
let startDate: string | Date;
let endDate: string | Date;
try {
// Convert to Date objects first to validate, then use ISO strings
const startDateObj = new Date(event.start_datetime);
const endDateObj = new Date(event.end_datetime);
if (isNaN(startDateObj.getTime()) || isNaN(endDateObj.getTime())) {
2025-08-13 13:51:27 +02:00
console.error('[EventCalendar] Invalid date values for event:', calendarId, {
2025-08-13 12:27:21 +02:00
start: event.start_datetime,
end: event.end_datetime
});
// Use fallback dates
startDate = new Date().toISOString();
endDate = new Date(Date.now() + 3600000).toISOString(); // +1 hour
} else {
startDate = startDateObj.toISOString();
endDate = endDateObj.toISOString();
}
} catch (error) {
2025-08-13 13:51:27 +02:00
console.error('[EventCalendar] Date parsing error for event:', calendarId, error);
2025-08-13 12:27:21 +02:00
// Use fallback dates
startDate = new Date().toISOString();
endDate = new Date(Date.now() + 3600000).toISOString(); // +1 hour
}
const transformedEvent = {
2025-08-13 13:51:27 +02:00
id: calendarId, // ✅ Use event_id instead of event.id
title: event.title,
2025-08-13 12:27:21 +02:00
start: startDate,
end: endDate,
backgroundColor: colors.bg,
borderColor: colors.border,
textColor: '#ffffff',
extendedProps: {
2025-08-13 12:27:21 +02:00
originalEvent: event, // Store original event for debugging
description: event.description,
location: event.location,
event_type: event.event_type,
is_paid: event.is_paid === 'true',
cost_members: event.cost_members,
cost_non_members: event.cost_non_members,
max_attendees: event.max_attendees ? parseInt(event.max_attendees) : undefined,
current_attendees: typeof event.current_attendees === 'string' ? parseInt(event.current_attendees) : (event.current_attendees || 0),
user_rsvp: event.user_rsvp,
visibility: event.visibility,
2025-08-13 13:51:27 +02:00
creator: event.creator,
event_id: event.event_id, // Store for reference
database_id: event.id || (event as any).Id
}
};
2025-08-13 12:27:21 +02:00
console.log('[EventCalendar] Transformed event result:', {
id: transformedEvent.id,
title: transformedEvent.title,
start: transformedEvent.start,
end: transformedEvent.end,
backgroundColor: transformedEvent.backgroundColor
});
return transformedEvent;
}
// Public methods
function getCalendarApi() {
return fullCalendar.value?.getApi();
}
function refetchEvents() {
const api = getCalendarApi();
if (api) {
api.refetchEvents();
}
}
function changeView(viewType: string) {
const api = getCalendarApi();
if (api) {
api.changeView(viewType);
}
}
function gotoDate(date: string | Date) {
const api = getCalendarApi();
if (api) {
api.gotoDate(date);
}
}
// Watch for mobile view changes
watch(mobileView, (newView) => {
2025-08-13 15:57:34 +02:00
let viewType;
switch (newView) {
case 'week': viewType = 'dayGridWeek'; break;
case 'list': viewType = 'listWeek'; break;
case 'month':
default: viewType = 'dayGridMonth'; break;
}
changeView(viewType);
});
// Expose methods to parent components
defineExpose({
getCalendarApi,
refetchEvents,
changeView,
gotoDate
});
</script>
<style scoped>
.event-calendar :deep(.fc) {
font-family: 'Roboto', sans-serif;
}
.event-calendar :deep(.fc-theme-standard .fc-scrollgrid) {
border-color: rgba(0, 0, 0, 0.12);
}
.event-calendar :deep(.fc-theme-standard td),
.event-calendar :deep(.fc-theme-standard th) {
border-color: rgba(0, 0, 0, 0.12);
}
.event-calendar :deep(.fc-button-primary) {
background-color: #a31515;
border-color: #a31515;
font-weight: 500;
text-transform: none;
}
.event-calendar :deep(.fc-button-primary:hover) {
background-color: #8b1212;
border-color: #8b1212;
}
.event-calendar :deep(.fc-button-primary:disabled) {
background-color: rgba(163, 21, 21, 0.5);
border-color: rgba(163, 21, 21, 0.5);
}
.event-calendar :deep(.fc-today-button) {
font-weight: 500;
text-transform: none;
}
.event-calendar :deep(.fc-toolbar-title) {
font-size: 1.25rem;
font-weight: 600;
color: #a31515;
}
.event-calendar :deep(.fc-day-today) {
background-color: rgba(163, 21, 21, 0.05) !important;
}
.event-calendar :deep(.fc-event) {
border-radius: 4px;
font-weight: 500;
font-size: 0.875rem;
cursor: pointer;
}
.event-calendar :deep(.fc-event:hover) {
opacity: 0.85;
}
.event-calendar :deep(.fc-paid-event) {
border-left: 4px solid #ff9800 !important;
}
.event-calendar :deep(.fc-user-rsvp) {
box-shadow: 0 0 0 2px #4caf50;
}
.event-calendar :deep(.fc-list-event-title) {
font-weight: 500;
}
.event-calendar :deep(.fc-list-event-time) {
font-weight: 600;
color: #a31515;
}
/* Mobile optimizations */
@media (max-width: 600px) {
.event-calendar :deep(.fc-toolbar) {
flex-direction: column;
gap: 8px;
}
.event-calendar :deep(.fc-toolbar-chunk) {
display: flex;
justify-content: center;
}
.event-calendar :deep(.fc-button-group) {
display: flex;
}
.event-calendar :deep(.fc-button) {
padding: 4px 8px;
font-size: 0.75rem;
}
.event-calendar :deep(.fc-toolbar-title) {
font-size: 1.1rem;
text-align: center;
}
}
</style>